From 316234cedb6fd75ff9b076be25a58959451768c1 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 15 Mar 2026 12:01:45 +0300 Subject: [PATCH 01/11] feat(storybook): enhance Vite configuration and add vitest setup - Added `setupFiles` to Vite configuration for Vitest integration. - Introduced `vitest.setup.ts` to configure Storybook preview annotations. - Updated Storybook preview to include `withPlainValue` decorator. - Refactored Drag stories to utilize new props and improve structure. --- .../react/storybook/.storybook/preview.ts | 3 ++ .../storybook/src/pages/Drag/Drag.stories.tsx | 52 +++++++------------ .../src/shared/lib/withPlainValue.tsx | 30 ++++++++--- packages/react/storybook/vite.config.ts | 1 + packages/react/storybook/vitest.setup.ts | 5 ++ 5 files changed, 52 insertions(+), 39 deletions(-) create mode 100644 packages/react/storybook/vitest.setup.ts diff --git a/packages/react/storybook/.storybook/preview.ts b/packages/react/storybook/.storybook/preview.ts index 1dbcd053..45b8b16e 100644 --- a/packages/react/storybook/.storybook/preview.ts +++ b/packages/react/storybook/.storybook/preview.ts @@ -1,6 +1,9 @@ import type {Preview} from '@storybook/react-vite' +import {withPlainValue} from '../src/shared/lib/withPlainValue' + const preview: Preview = { + decorators: [withPlainValue], globalTypes: { showPlainValue: { name: 'Plain Value', diff --git a/packages/react/storybook/src/pages/Drag/Drag.stories.tsx b/packages/react/storybook/src/pages/Drag/Drag.stories.tsx index 96b43270..599b4869 100644 --- a/packages/react/storybook/src/pages/Drag/Drag.stories.tsx +++ b/packages/react/storybook/src/pages/Drag/Drag.stories.tsx @@ -1,12 +1,11 @@ import {MarkedInput, useMark} from '@markput/react' import type {MarkProps, MarkedInputProps, Option} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' -import type {CSSProperties, ReactNode} from 'react' +import type {CSSProperties} from 'react' import {useState} from 'react' import {Text} from '../../shared/components/Text' import {DRAG_MARKDOWN} from '../../shared/lib/sampleTexts' -import {withPlainValue} from '../../shared/lib/withPlainValue' import {markdownOptions} from '../Nested/MarkdownOptions' export default { @@ -27,33 +26,21 @@ type Story = StoryObj> // ─── Markdown with block-level marks (headings + list) ──────────────────────── -const MarkdownMark = ({ - children, - value, - style, -}: { - value?: string - children?: ReactNode - style?: React.CSSProperties -}) => {children || value} - -export const Markdown: Story = { - render: () => { - const [value, setValue] = useState(DRAG_MARKDOWN) +interface MarkdownMarkProps extends MarkProps { + style?: CSSProperties +} - return ( -
- - -
- ) +const MarkdownMark = ({children, value, style}: MarkdownMarkProps) => ( + {children || value} +) + +export const Markdown: StoryObj> = { + args: { + Mark: MarkdownMark, + options: markdownOptions, + value: DRAG_MARKDOWN, + drag: true, + style: {minHeight: 300, padding: 12, border: '1px solid #e0e0e0', borderRadius: 8}, }, } @@ -193,7 +180,7 @@ const TODO_VALUE = `# \u{1F4CB} Project Launch Checklist const testStyle: React.CSSProperties = {minHeight: 100, padding: 8, border: '1px solid #e0e0e0'} export const PlainTextDrag: Story = { - parameters: {docs: {disable: true}}, + parameters: {docs: {disable: true}, plainValue: false}, render: () => { const [value, setValue] = useState( 'First block of plain text\n\nSecond block of plain text\n\nThird block of plain text\n\nFourth block of plain text\n\nFifth block of plain text' @@ -207,8 +194,8 @@ export const PlainTextDrag: Story = { }, } -export const MarkdownDrag: Story = { - parameters: {docs: {disable: true}}, +export const MarkdownDrag: StoryObj> = { + parameters: {docs: {disable: true}, plainValue: false}, render: () => { const [value, setValue] = useState( '# Welcome to Draggable Blocks\n\nThis is the first paragraph.\n\nThis is the second paragraph.\n\n## Features\n\n- Drag handles appear on hover' @@ -230,14 +217,13 @@ export const MarkdownDrag: Story = { } export const ReadOnlyDrag: Story = { - parameters: {docs: {disable: true}}, + parameters: {docs: {disable: true}, plainValue: false}, render: () => , } // ─── Todo list (all marks include \n\n) ─────────────────────────────────────── export const TodoList: StoryObj> = { - decorators: [withPlainValue], args: { Mark: TodoMark, options: todoOptions, diff --git a/packages/react/storybook/src/shared/lib/withPlainValue.tsx b/packages/react/storybook/src/shared/lib/withPlainValue.tsx index 3b72165a..6d925741 100644 --- a/packages/react/storybook/src/shared/lib/withPlainValue.tsx +++ b/packages/react/storybook/src/shared/lib/withPlainValue.tsx @@ -1,22 +1,40 @@ -import {useCallback} from 'react' +import {useCallback, useState} from 'react' import {useArgs, useGlobals} from 'storybook/preview-api' import {Text} from '../components/Text' -export const withPlainValue = (Story: any) => { +export const withPlainValue = (Story: any, context: any) => { + if (context.parameters?.plainValue === false) { + return + } const [args, updateArgs] = useArgs() const [globals] = useGlobals() + const isControlled = 'value' in args const showPlainValue = globals.showPlainValue !== 'hide' + // displayValue tracks onChange synchronously so stays up-to-date in + // tests where updateArgs propagation is async. + const [displayValue, setDisplayValue] = useState(args.value) + const handleChange = useCallback( (newValue: string) => { + setDisplayValue(newValue) updateArgs({value: newValue}) }, [updateArgs] ) + // Only wrap controlled stories (those with `value` in args). + // Uncontrolled stories use `defaultValue` — overriding onChange would inject + // `value` back via updateArgs, switching them to controlled mode. + if (!isControlled) { + return + } + + const storyArgs = {...args, onChange: handleChange} + if (!showPlainValue) { - return + return } return ( @@ -30,11 +48,11 @@ export const withPlainValue = (Story: any) => { }} >
- +
- {args.value !== undefined && ( + {displayValue !== undefined && (
- +
)} diff --git a/packages/react/storybook/vite.config.ts b/packages/react/storybook/vite.config.ts index 6935da94..cf817cd0 100644 --- a/packages/react/storybook/vite.config.ts +++ b/packages/react/storybook/vite.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ }, test: { globals: true, + setupFiles: ['./vitest.setup.ts'], include: ['src/pages/**/*.spec.ts', 'src/pages/**/*.spec.tsx'], coverage: { provider: 'v8', diff --git a/packages/react/storybook/vitest.setup.ts b/packages/react/storybook/vitest.setup.ts new file mode 100644 index 00000000..21a8217c --- /dev/null +++ b/packages/react/storybook/vitest.setup.ts @@ -0,0 +1,5 @@ +import {setProjectAnnotations} from '@storybook/react-vite' + +import * as preview from './.storybook/preview' + +setProjectAnnotations(preview) \ No newline at end of file From 98ee465cf3c123aa85078e0316de6afcc2c8db4b Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 15 Mar 2026 12:32:00 +0300 Subject: [PATCH 02/11] refactor(storybook): convert stories to use StoryObj type and improve structure - Updated stories in various components to utilize `StoryObj` for better type safety and consistency. - Refactored story exports to use args for props, enhancing readability and maintainability. - Removed unnecessary state management in favor of args, streamlining the story definitions. - Improved overall structure and organization of story files for clarity. --- .../storybook/src/pages/Ant/Ant.stories.tsx | 41 +-- .../storybook/src/pages/Base/Base.spec.tsx | 3 +- .../src/pages/Dynamic/Dynamic.stories.tsx | 47 +-- .../src/pages/Material/Material.stories.tsx | 45 ++- .../src/pages/Nested/Nested.stories.tsx | 276 +++++---------- .../src/pages/Overlay/Overlay.stories.tsx | 57 ++- .../src/pages/Rsuite/Rsuite.stories.tsx | 84 ++--- .../src/pages/Slots/Slots.stories.tsx | 331 ++++++++---------- 8 files changed, 375 insertions(+), 509 deletions(-) diff --git a/packages/react/storybook/src/pages/Ant/Ant.stories.tsx b/packages/react/storybook/src/pages/Ant/Ant.stories.tsx index 8c2711a2..e12ffbd7 100644 --- a/packages/react/storybook/src/pages/Ant/Ant.stories.tsx +++ b/packages/react/storybook/src/pages/Ant/Ant.stories.tsx @@ -1,34 +1,25 @@ +import type {MarkProps} from '@markput/react' import {MarkedInput} from '@markput/react' +import type {Meta, StoryObj} from '@storybook/react-vite' import {Tag} from 'antd' -import {useState} from 'react' - -import {Text} from '../../shared/components/Text' +import type {ComponentType} from 'react' export default { title: 'Styled/Ant', component: MarkedInput, -} - -export const Tagged = () => { - const [value, setValue] = useState( - `We preset five different colors. You can set color property such as @(success), @(processing), @(error), @(default) and @(warning) to show specific status.` - ) +} satisfies Meta - return ( - <> - ({children: value, color: value, style: {marginRight: 0}}), - }, - ]} - /> +type Story = StoryObj - - - ) +export const Tagged: Story = { + args: { + Mark: Tag as ComponentType, + value: `We preset five different colors. You can set color property such as @(success), @(processing), @(error), @(default) and @(warning) to show specific status.`, + options: [ + { + markup: '@(__value__)', + mark: ({value}: MarkProps) => ({children: value, color: value, style: {marginRight: 0}}), + }, + ], + }, } \ No newline at end of file diff --git a/packages/react/storybook/src/pages/Base/Base.spec.tsx b/packages/react/storybook/src/pages/Base/Base.spec.tsx index 497f7b70..6074e8bb 100644 --- a/packages/react/storybook/src/pages/Base/Base.spec.tsx +++ b/packages/react/storybook/src/pages/Base/Base.spec.tsx @@ -6,12 +6,13 @@ import {render} from 'vitest-browser-react' import {page, userEvent} from 'vitest/browser' import {focusAtEnd, focusAtStart} from '../../shared/lib/focus' -import {Focusable, Removable} from '../Dynamic/Dynamic.stories' +import * as DynamicStories from '../Dynamic/Dynamic.stories' import * as BaseStories from './Base.stories' //createVisualTests(BaseStories) const {Default} = composeStories(BaseStories) +const {Focusable, Removable} = composeStories(DynamicStories) describe(`Component: MarkedInput`, () => { it.todo('should set readOnly on selection') diff --git a/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx b/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx index 8c799525..bed98981 100644 --- a/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx +++ b/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx @@ -1,14 +1,16 @@ import {MarkedInput, useMark} from '@markput/react' -import {useEffect, useState} from 'react' +import type {Meta, StoryObj} from '@storybook/react-vite' +import {useEffect} from 'react' -import {Text} from '../../shared/components/Text' import {useCaretInfo} from '../../shared/hooks/useCaretInfo' export default { title: 'MarkedInput/Mark', tags: ['autodocs'], component: MarkedInput, -} +} satisfies Meta + +type Story = StoryObj const Mark = () => { const {value, ref} = useMark() @@ -20,9 +22,11 @@ const Mark = () => { return } -export const Dynamic = () => { - const [value, setValue] = useState('Hello, dynamical mark @[world]( )!') - return +export const Dynamic: Story = { + args: { + Mark, + value: 'Hello, dynamical mark @[world]( )!', + }, } const RemovableMark = () => { @@ -30,9 +34,12 @@ const RemovableMark = () => { return } -export const Removable = () => { - const [value, setValue] = useState('I @[contain]( ) @[removable]( ) by click @[marks]( )!') - return +export const Removable: Story = { + parameters: {docs: {disable: true}, plainValue: false}, + args: { + Mark: RemovableMark, + value: 'I @[contain]( ) @[removable]( ) by click @[marks]( )!', + }, } const Abbr = () => { @@ -55,16 +62,16 @@ const Abbr = () => { ) } -export const Focusable = () => { - const [value, setValue] = useState('Hello, @[focusable](By key operations) abbreviation @[world](Hello! Hello!)!') - useCaretInfo(true) - - return ( - <> - - - - ) +export const Focusable: Story = { + parameters: {docs: {disable: true}}, + args: { + Mark: Abbr, + value: 'Hello, @[focusable](By key operations) abbreviation @[world](Hello! Hello!)!', + }, + render: args => { + useCaretInfo(true) + return + }, } /*TODO @@ -92,7 +99,7 @@ export const RichEditor = () => { This feature allows you to use dynamic marks to edit itself and beyond. -It can be used to simulate a rich editor with bold>, italic>, marked>, smaller>, deleted>, +It can be used to simulate a rich editor with bold>, italic>, marked>, smaller>, deleted>, inserted>, subscript> and other types of text.`) return ( diff --git a/packages/react/storybook/src/pages/Material/Material.stories.tsx b/packages/react/storybook/src/pages/Material/Material.stories.tsx index dfaf2694..2793d7fd 100644 --- a/packages/react/storybook/src/pages/Material/Material.stories.tsx +++ b/packages/react/storybook/src/pages/Material/Material.stories.tsx @@ -1,6 +1,8 @@ import type {MarkProps} from '@markput/react' import {MarkedInput} from '@markput/react' import {Chip, Input} from '@mui/material' +import type {Meta, StoryObj} from '@storybook/react-vite' +import type {ComponentType} from 'react' import {useState} from 'react' import {Text} from '../../shared/components/Text' @@ -9,7 +11,9 @@ import {MaterialMentions} from './components/MaterialMentions' export default { title: 'Styled/Material', component: MarkedInput, -} +} satisfies Meta + +type Story = StoryObj export const Mentions = () => { const [value, setValue] = useState(`Enter the '@' for calling mention list: \n- Hello @Agustina and @[Ruslan]!`) @@ -25,30 +29,21 @@ export const Mentions = () => { const initialValue = 'Hello beautiful the @[first](outlined:1) world from the @[second](common:2) ' -export const Chipped = () => { - const [value, setValue] = useState(initialValue) - - return ( - <> - ({label: value, variant: 'outlined' as const, size: 'small' as const}), - }, - { - markup: '@[__value__](common:__meta__)', - mark: ({value}) => ({label: value, size: 'small' as const}), - }, - ]} - /> - - - - ) +export const Chipped: Story = { + args: { + Mark: Chip as ComponentType, + value: initialValue, + options: [ + { + markup: '@[__value__](outlined:__meta__)', + mark: ({value}: MarkProps) => ({label: value, variant: 'outlined' as const, size: 'small' as const}), + }, + { + markup: '@[__value__](common:__meta__)', + mark: ({value}: MarkProps) => ({label: value, size: 'small' as const}), + }, + ], + }, } export const Overridden = () => { diff --git a/packages/react/storybook/src/pages/Nested/Nested.stories.tsx b/packages/react/storybook/src/pages/Nested/Nested.stories.tsx index aab847d5..27821220 100644 --- a/packages/react/storybook/src/pages/Nested/Nested.stories.tsx +++ b/packages/react/storybook/src/pages/Nested/Nested.stories.tsx @@ -1,11 +1,10 @@ -import type {Markup} from '@markput/react' +import type {MarkProps, Markup} from '@markput/react' import {MarkedInput, useMark} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' -import type {ReactNode} from 'react' +import type {ComponentType, ReactNode} from 'react' import {useState} from 'react' import {useTab} from '../../shared/components/Tabs' -import {Text} from '../../shared/components/Text' import {COMPLEX_MARKDOWN} from '../../shared/lib/sampleTexts' import {markdownOptions as MarkdownOptions} from './MarkdownOptions' @@ -37,37 +36,27 @@ const SimpleMark = ({children, style, value}: {value?: string; children?: ReactN ) export const SimpleNesting: Story = { - render: () => { - const [value, setValue] = useState('This is *italic text with **bold** inside* and more text.') - - return ( - <> - ({ - value, - children, - style: {fontWeight: 'bold'}, - }), - }, - { - markup: ItalicMarkup, - mark: ({value, children}) => ({ - value, - children, - style: {fontStyle: 'italic'}, - }), - }, - ]} - /> - - - ) + args: { + Mark: SimpleMark as ComponentType, + value: 'This is *italic text with **bold** inside* and more text.', + options: [ + { + markup: BoldMarkup, + mark: ({value, children}: MarkProps) => ({ + value, + children, + style: {fontWeight: 'bold'}, + }), + }, + { + markup: ItalicMarkup, + mark: ({value, children}: MarkProps) => ({ + value, + children, + style: {fontStyle: 'italic'}, + }), + }, + ], }, } @@ -90,65 +79,53 @@ const MultiLevelMark = ({ }) => {children || value} export const MultipleLevels: Story = { - render: () => { - const [value, setValue] = useState( - 'Check #[this tag with @[nested mention with `code`]] and #[another #[deeply nested] tag]' - ) - - return ( - <> - ({ - value, - children, - style: { - backgroundColor: '#e7f3ff', - border: '1px solid #2196f3', - color: '#1976d2', - padding: '2px 6px', - borderRadius: '4px', - }, - }), - }, - { - markup: MentionMarkup, - mark: ({value, children}) => ({ - value, - children, - style: { - backgroundColor: '#fff3e0', - border: '1px solid #ff9800', - color: '#f57c00', - padding: '2px 6px', - borderRadius: '4px', - }, - }), - }, - { - markup: CodeMarkup, - mark: ({value, children}) => ({ - value, - children, - style: { - backgroundColor: '#f3e5f5', - border: '1px solid #9c27b0', - color: '#7b1fa2', - padding: '2px 6px', - borderRadius: '4px', - }, - }), - }, - ]} - /> - - - ) + args: { + Mark: MultiLevelMark as ComponentType, + value: 'Check #[this tag with @[nested mention with `code`]] and #[another #[deeply nested] tag]', + options: [ + { + markup: TagMarkup, + mark: ({value, children}: MarkProps) => ({ + value, + children, + style: { + backgroundColor: '#e7f3ff', + border: '1px solid #2196f3', + color: '#1976d2', + padding: '2px 6px', + borderRadius: '4px', + }, + }), + }, + { + markup: MentionMarkup, + mark: ({value, children}: MarkProps) => ({ + value, + children, + style: { + backgroundColor: '#fff3e0', + border: '1px solid #ff9800', + color: '#f57c00', + padding: '2px 6px', + borderRadius: '4px', + }, + }), + }, + { + markup: CodeMarkup, + mark: ({value, children}: MarkProps) => ({ + value, + children, + style: { + backgroundColor: '#f3e5f5', + border: '1px solid #9c27b0', + color: '#7b1fa2', + padding: '2px 6px', + borderRadius: '4px', + }, + }), + }, + ], }, } @@ -164,17 +141,10 @@ const HtmlLikeMark = ({children, value, nested}: {value?: string; children?: Rea } export const HtmlLikeTags: Story = { - render: () => { - const [value, setValue] = useState( - '
This is a div with a mark inside and bold text with nested del
' - ) - - return ( - <> - - - - ) + args: { + Mark: HtmlLikeMark as ComponentType, + value: '
This is a div with a mark inside and bold text with nested del
', + options: [{markup: HtmlMarkup}], }, } @@ -217,75 +187,13 @@ const InteractiveMark = ({children, nested}: {value?: string; children?: ReactNo } export const InteractiveNested: Story = { - render: () => { - const [value, setValue] = useState('@[Click me @[or me @[or even me]]]') - - return ( - <> - - - - ) + args: { + Mark: InteractiveMark as ComponentType, + value: '@[Click me @[or me @[or even me]]]', + options: [{markup: '@[__nested__]'}], }, } -// ============================================================================ -// Example 5: Editable Nested Content -// ============================================================================ - -// const EditableMark = ({children}: {value?: string; children?: ReactNode}) => { -// const mark = useMark() -// -// return ( -// -// {children} -// -// ) -// } - -// TODO fix editable nested marks support -// const EditableNested: Story = { -// render: () => { -// const [value, setValue] = useState('@[Edit this @[and this @[and even this]]]') -// -// return ( -// <> -// -// -//
-//

-// Instructions: Click inside any mark to edit its content. Nested marks are -// independently editable. -//

-//
-// -// ) -// }, -// } - // ============================================================================ // Example 6: Complex Markdown Document // ============================================================================ @@ -301,8 +209,10 @@ const MarkdownMark = ({ }) => {children || value} export const ComplexMarkdown: Story = { - render: () => { - const [value, setValue] = useState(COMPLEX_MARKDOWN) + args: { + value: COMPLEX_MARKDOWN, + }, + render: args => { const {Tab, activeTab} = useTab([ {value: 'preview', label: 'Preview'}, {value: 'write', label: 'Write'}, @@ -313,9 +223,9 @@ export const ComplexMarkdown: Story = { {activeTab === 'preview' ? ( - + ) : ( - + )} ) @@ -480,8 +390,8 @@ const HtmlDocMark = ({children, value, nested}: {value?: string; children?: Reac } export const ComplexHtmlDocument: Story = { - render: () => { - const [value, setValue] = useState(`
+ args: { + value: `

Understanding Nested HTML Structures

Published on

@@ -541,7 +451,9 @@ export const ComplexHtmlDocument: Story = {

© 2025 MarkedInput Library. Built with React and TypeScript.

-
`) +
`, + }, + render: args => { const {Tab, activeTab} = useTab([ {value: 'preview', label: 'Preview'}, {value: 'write', label: 'Write'}, @@ -555,18 +467,14 @@ export const ComplexHtmlDocument: Story = { ) : ( - + )} ) }, -} - -// ============================================================================ -// Example 8: Complex Real-World Example (Rich Text Editor) -// ============================================================================ \ No newline at end of file +} \ No newline at end of file diff --git a/packages/react/storybook/src/pages/Overlay/Overlay.stories.tsx b/packages/react/storybook/src/pages/Overlay/Overlay.stories.tsx index 507e981f..85b22d74 100644 --- a/packages/react/storybook/src/pages/Overlay/Overlay.stories.tsx +++ b/packages/react/storybook/src/pages/Overlay/Overlay.stories.tsx @@ -2,7 +2,6 @@ import type {MarkToken} from '@markput/react' import {MarkedInput, useOverlay} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' import type {RefObject} from 'react' -import {useState} from 'react' export default { title: 'MarkedInput/Overlay', @@ -30,31 +29,35 @@ export const DefaultOverlay: Story = { } const Overlay = () =>

I am the overlay

-export const CustomOverlay = () => { - const [value, setValue] = useState('Hello, custom overlay by trigger @!') - return null} Overlay={Overlay} value={value} onChange={setValue} /> + +export const CustomOverlay: Story = { + args: { + Mark: () => null, + Overlay, + value: 'Hello, custom overlay by trigger @!', + }, } -export const CustomTrigger = () => { - const [value, setValue] = useState('Hello, custom overlay by trigger /!') - return ( - null} - Overlay={Overlay} - value={value} - onChange={setValue} - options={[{overlay: {trigger: '/'}}]} - /> - ) +export const CustomTrigger: Story = { + args: { + Mark: () => null, + Overlay, + value: 'Hello, custom overlay by trigger /!', + options: [{overlay: {trigger: '/'}}], + }, } const Tooltip = () => { const {style} = useOverlay() return
I am the overlay
} -export const PositionedOverlay = () => { - const [value, setValue] = useState('Hello, positioned overlay by trigger @!') - return null} Overlay={Tooltip} value={value} onChange={setValue} /> + +export const PositionedOverlay: Story = { + args: { + Mark: () => null, + Overlay: Tooltip, + value: 'Hello, positioned overlay by trigger @!', + }, } const List = () => { @@ -67,15 +70,11 @@ const List = () => { ) } -export const SelectableOverlay = () => { - const [value, setValue] = useState('Hello, suggest overlay by trigger @!') - return ( - - ) +export const SelectableOverlay: Story = { + args: { + Mark, + Overlay: List, + value: 'Hello, suggest overlay by trigger @!', + options: [{markup: '@[__value__](__meta__)', overlay: {trigger: '@'}}], + }, } \ No newline at end of file diff --git a/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx b/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx index 91641ad9..5f540480 100644 --- a/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx +++ b/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx @@ -1,6 +1,7 @@ -import type {Markup} from '@markput/react' +import type {MarkProps, Markup} from '@markput/react' import {MarkedInput, useOverlay} from '@markput/react' -import type {Meta} from '@storybook/react-vite' +import type {Meta, StoryObj} from '@storybook/react-vite' +import type {ComponentType} from 'react' import {useEffect, useState} from 'react' import {Input, Popover, Tag} from 'rsuite' @@ -13,6 +14,8 @@ export default { decorators: [withStyle('rsuite.min.css')], } as Meta +type Story = StoryObj> + const Overlay = () => { const {style, match, select, close} = useOverlay() @@ -67,48 +70,37 @@ export const Overridden = () => { ) } -export const TaggedInput = () => { - const [value, setValue] = useState(initialState) - const classNames = 'rs-picker-tag-wrapper rs-picker-input rs-picker-toggle-wrapper rs-picker-tag' - - return ( - <> - ({children: value, style: {marginLeft: 0}}), - overlay: {trigger: '@'}, - }, - ]} - slotProps={{ - container: { - onKeyDown: e => e.key === 'Enter' && e.preventDefault(), - }, - span: { - className: 'rs-tag rs-tag-md', - style: { - backgroundColor: 'white', - paddingLeft: 0, - paddingRight: 0, - whiteSpace: 'pre-wrap', - minWidth: 5, - }, - }, - }} - /> - -
- - - ) +export const TaggedInput: Story = { + args: { + Mark: Tag as ComponentType, + Overlay, + value: initialState, + className: 'rs-picker-tag-wrapper rs-picker-input rs-picker-toggle-wrapper rs-picker-tag', + style: { + minHeight: 36, + paddingRight: 5, + }, + options: [ + { + markup: '@[__value__](common)' as Markup, + mark: ({value}: MarkProps) => ({children: value, style: {marginLeft: 0}}), + overlay: {trigger: '@'}, + }, + ], + slotProps: { + container: { + onKeyDown: (e: React.KeyboardEvent) => e.key === 'Enter' && e.preventDefault(), + }, + span: { + className: 'rs-tag rs-tag-md', + style: { + backgroundColor: 'white', + paddingLeft: 0, + paddingRight: 0, + whiteSpace: 'pre-wrap', + minWidth: 5, + }, + }, + }, + }, } \ No newline at end of file diff --git a/packages/react/storybook/src/pages/Slots/Slots.stories.tsx b/packages/react/storybook/src/pages/Slots/Slots.stories.tsx index f49e80da..99c5deb8 100644 --- a/packages/react/storybook/src/pages/Slots/Slots.stories.tsx +++ b/packages/react/storybook/src/pages/Slots/Slots.stories.tsx @@ -1,9 +1,8 @@ import {MarkedInput} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' +import type {ComponentType} from 'react' import {useState} from 'react' -import {Text} from '../../shared/components/Text' - const meta = { title: 'API/Slots', component: MarkedInput, @@ -25,67 +24,54 @@ const SimpleMark = ({children}: {value?: string; children?: React.ReactNode}) => {children} ) +const FancyContainer = ({ref, ...props}: React.HTMLAttributes & {ref?: React.Ref}) => ( +
+) + +const FancySpan = ({ref, ...props}: React.HTMLAttributes & {ref?: React.Ref}) => ( + +) + /** * Using slots to completely replace container and span components. * This is useful when you need full control over the component structure. */ export const CustomComponents: Story = { - render: () => { - const [value, setValue] = useState('Both @[container] and @[span] are @[customized]') - - const FancyContainer = ({ - ref, - ...props - }: React.HTMLAttributes & {ref?: React.Ref}) => ( -
- ) - - const FancySpan = ({ - ref, - ...props - }: React.HTMLAttributes & {ref?: React.Ref}) => ( - - ) - - return ( - <> -

Custom Components via Slots

-

Replace default div/span with your own components:

- - - - - - ) + args: { + Mark: SimpleMark as ComponentType, + value: 'Both @[container] and @[span] are @[customized]', + slots: { + container: FancyContainer, + span: FancySpan, + }, }, + render: args => ( + <> +

Custom Components via Slots

+

Replace default div/span with your own components:

+ + + ), } /** @@ -93,50 +79,56 @@ export const CustomComponents: Story = { * This is useful when you want to keep the default components but customize their behavior. */ export const WithSlotProps: Story = { - render: () => { - const [value, setValue] = useState('Try pressing @[Enter] or clicking') + args: { + Mark: SimpleMark as ComponentType, + value: 'Try pressing @[Enter] or clicking', + className: 'custom-container', + slotProps: { + container: { + style: { + border: '2px solid #4CAF50', + borderRadius: '8px', + padding: '12px', + backgroundColor: '#f5f5f5', + }, + }, + span: { + style: { + color: '#333', + fontSize: '14px', + }, + }, + }, + }, + render: args => { const [events, setEvents] = useState([]) const addEvent = (event: string) => { setEvents(prev => [...prev.slice(-4), event]) } + const slotProps = { + ...args.slotProps, + container: { + ...args.slotProps?.container, + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + addEvent('Enter pressed') + } + }, + onClick: () => addEvent('Clicked'), + onFocus: () => addEvent('Focused'), + onBlur: () => addEvent('Blurred'), + }, + } + return ( <>

Styling & Events via slotProps

Customize styling and add custom event handlers without replacing components:

- { - if (e.key === 'Enter') { - e.preventDefault() - addEvent('Enter pressed') - } - }, - onClick: () => addEvent('Clicked'), - onFocus: () => addEvent('Focused'), - onBlur: () => addEvent('Blurred'), - style: { - border: '2px solid #4CAF50', - borderRadius: '8px', - padding: '12px', - backgroundColor: '#f5f5f5', - }, - }, - span: { - style: { - color: '#333', - fontSize: '14px', - }, - }, - }} - /> +
Recent events: @@ -150,65 +142,52 @@ export const WithSlotProps: Story = { )}
- - ) }, } +const StyledContainer = ({ref, ...props}: React.HTMLAttributes & {ref?: React.Ref}) => ( +
+) + /** * Edge case: Style merging when both slots and slotProps provide styles. * Shows that slotProps styles take precedence. */ export const StyleMerging: Story = { - render: () => { - const [value, setValue] = useState('Container has @[merged] styles from multiple sources') - - const StyledContainer = ({ - ref, - ...props - }: React.HTMLAttributes & {ref?: React.Ref}) => ( -
- ) - - return ( - <> -

Style Merging

-

- When using both slots and slotProps, styles merge intelligently. slotProps styles override slot - styles: -

- - - - - - ) + args: { + Mark: SimpleMark as ComponentType, + value: 'Container has @[merged] styles from multiple sources', + slots: { + container: StyledContainer, + }, + slotProps: { + container: { + style: { + padding: '16px', + border: '2px solid #1976d2', + }, + }, + }, }, + render: args => ( + <> +

Style Merging

+

+ When using both slots and slotProps, styles merge intelligently. slotProps styles override slot styles: +

+ + + ), } /** @@ -216,46 +195,40 @@ export const StyleMerging: Story = { * React automatically converts camelCase properties like 'dataUserId' to HTML attributes like 'data-user-id'. */ export const DataAttributes: Story = { - render: () => { - const [value, setValue] = useState('Use @[data] attributes for testing and tracking') - - return ( - <> -

Data Attributes in camelCase

-

slotProps supports camelCase data attributes (React converts them to kebab-case):

- - - -
-

- Try inspecting the element: You'll see data-test-id, data-module, data-user-id - attributes on the container. These are automatically converted from camelCase (dataTestId → - data-test-id). -

-
- - - - ) + args: { + Mark: SimpleMark as ComponentType, + value: 'Use @[data] attributes for testing and tracking', + slotProps: { + container: { + dataTestId: 'marked-input-demo', + dataModule: 'slots-api', + dataUserId: 'user-123', + style: { + border: '1px solid #999', + padding: '12px', + borderRadius: '4px', + backgroundColor: '#f9f9f9', + }, + }, + span: { + dataTokenType: 'text', + }, + }, }, + render: args => ( + <> +

Data Attributes in camelCase

+

slotProps supports camelCase data attributes (React converts them to kebab-case):

+ + + +
+

+ Try inspecting the element: You'll see data-test-id, data-module, data-user-id + attributes on the container. These are automatically converted from camelCase (dataTestId → + data-test-id). +

+
+ + ), } \ No newline at end of file From 9bb3e348fb97dd7b7aa80453461ad8f96877c163 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 15 Mar 2026 12:56:12 +0300 Subject: [PATCH 03/11] refactor(storybook): enhance type safety in stories with MarkedInputProps - Updated story exports to utilize `MarkedInputProps` for better type safety across various components. - Refactored `Tagged`, `Chipped`, and other story definitions to improve clarity and maintainability. - Ensured consistent use of `StoryObj` type for all story exports, enhancing overall structure. --- .../storybook/src/pages/Ant/Ant.stories.tsx | 9 +-- .../src/pages/Material/Material.stories.tsx | 9 +-- .../src/pages/Nested/Nested.stories.tsx | 73 +++++++++---------- .../src/pages/Rsuite/Rsuite.stories.tsx | 9 +-- .../src/pages/Slots/Slots.stories.tsx | 21 +++--- 5 files changed, 58 insertions(+), 63 deletions(-) diff --git a/packages/react/storybook/src/pages/Ant/Ant.stories.tsx b/packages/react/storybook/src/pages/Ant/Ant.stories.tsx index e12ffbd7..1b190dfa 100644 --- a/packages/react/storybook/src/pages/Ant/Ant.stories.tsx +++ b/packages/react/storybook/src/pages/Ant/Ant.stories.tsx @@ -1,6 +1,7 @@ -import type {MarkProps} from '@markput/react' +import type {MarkProps, MarkedInputProps} from '@markput/react' import {MarkedInput} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' +import type {TagProps} from 'antd' import {Tag} from 'antd' import type {ComponentType} from 'react' @@ -9,11 +10,9 @@ export default { component: MarkedInput, } satisfies Meta -type Story = StoryObj - -export const Tagged: Story = { +export const Tagged: StoryObj> = { args: { - Mark: Tag as ComponentType, + Mark: Tag as ComponentType, value: `We preset five different colors. You can set color property such as @(success), @(processing), @(error), @(default) and @(warning) to show specific status.`, options: [ { diff --git a/packages/react/storybook/src/pages/Material/Material.stories.tsx b/packages/react/storybook/src/pages/Material/Material.stories.tsx index 2793d7fd..1ea6c263 100644 --- a/packages/react/storybook/src/pages/Material/Material.stories.tsx +++ b/packages/react/storybook/src/pages/Material/Material.stories.tsx @@ -1,5 +1,6 @@ -import type {MarkProps} from '@markput/react' +import type {MarkProps, MarkedInputProps} from '@markput/react' import {MarkedInput} from '@markput/react' +import type {ChipProps} from '@mui/material' import {Chip, Input} from '@mui/material' import type {Meta, StoryObj} from '@storybook/react-vite' import type {ComponentType} from 'react' @@ -13,8 +14,6 @@ export default { component: MarkedInput, } satisfies Meta -type Story = StoryObj - export const Mentions = () => { const [value, setValue] = useState(`Enter the '@' for calling mention list: \n- Hello @Agustina and @[Ruslan]!`) @@ -29,9 +28,9 @@ export const Mentions = () => { const initialValue = 'Hello beautiful the @[first](outlined:1) world from the @[second](common:2) ' -export const Chipped: Story = { +export const Chipped: StoryObj> = { args: { - Mark: Chip as ComponentType, + Mark: Chip as ComponentType, value: initialValue, options: [ { diff --git a/packages/react/storybook/src/pages/Nested/Nested.stories.tsx b/packages/react/storybook/src/pages/Nested/Nested.stories.tsx index 27821220..66aa50c6 100644 --- a/packages/react/storybook/src/pages/Nested/Nested.stories.tsx +++ b/packages/react/storybook/src/pages/Nested/Nested.stories.tsx @@ -1,14 +1,14 @@ -import type {MarkProps, Markup} from '@markput/react' +import type {MarkProps, MarkedInputProps, Markup} from '@markput/react' import {MarkedInput, useMark} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' -import type {ComponentType, ReactNode} from 'react' +import type {CSSProperties} from 'react' import {useState} from 'react' import {useTab} from '../../shared/components/Tabs' import {COMPLEX_MARKDOWN} from '../../shared/lib/sampleTexts' import {markdownOptions as MarkdownOptions} from './MarkdownOptions' -export default { +const meta = { title: 'MarkedInput/Nested', tags: ['autodocs'], component: MarkedInput, @@ -22,7 +22,8 @@ export default { }, } satisfies Meta -type Story = StoryObj> +export default meta +type Story = StoryObj // ============================================================================ // Example 1: Simple Nesting (Markdown-style) @@ -31,13 +32,15 @@ type Story = StoryObj> const BoldMarkup: Markup = '**__nested__**' const ItalicMarkup: Markup = '*__nested__*' -const SimpleMark = ({children, style, value}: {value?: string; children?: ReactNode; style?: React.CSSProperties}) => ( - {children || value} -) +interface SimpleMarkProps extends MarkProps { + style?: CSSProperties +} + +const SimpleMark = ({children, style, value}: SimpleMarkProps) => {children || value} -export const SimpleNesting: Story = { +export const SimpleNesting: StoryObj> = { args: { - Mark: SimpleMark as ComponentType, + Mark: SimpleMark, value: 'This is *italic text with **bold** inside* and more text.', options: [ { @@ -68,19 +71,17 @@ const TagMarkup: Markup = '#[__nested__]' const MentionMarkup: Markup = '@[__nested__]' const CodeMarkup: Markup = '`__nested__`' -const MultiLevelMark = ({ - children, - style, - value, -}: { - value?: string - children?: ReactNode - style?: React.CSSProperties -}) => {children || value} - -export const MultipleLevels: Story = { +interface MultiLevelMarkProps extends MarkProps { + style?: CSSProperties +} + +const MultiLevelMark = ({children, style, value}: MultiLevelMarkProps) => ( + {children || value} +) + +export const MultipleLevels: StoryObj> = { args: { - Mark: MultiLevelMark as ComponentType, + Mark: MultiLevelMark, value: 'Check #[this tag with @[nested mention with `code`]] and #[another #[deeply nested] tag]', options: [ { @@ -135,14 +136,14 @@ export const MultipleLevels: Story = { const HtmlMarkup: Markup = '<__value__>__nested__' -const HtmlLikeMark = ({children, value, nested}: {value?: string; children?: ReactNode; nested?: string}) => { +const HtmlLikeMark = ({children, value, nested}: MarkProps) => { const Tag = value! as React.ElementType return {children || nested} } -export const HtmlLikeTags: Story = { +export const HtmlLikeTags: StoryObj> = { args: { - Mark: HtmlLikeMark as ComponentType, + Mark: HtmlLikeMark, value: '
This is a div with a mark inside and bold text with nested del
', options: [{markup: HtmlMarkup}], }, @@ -152,7 +153,7 @@ export const HtmlLikeTags: Story = { // Example 4: Interactive Nested Marks with Tree Navigation // ============================================================================ -const InteractiveMark = ({children, nested}: {value?: string; children?: ReactNode; nested?: string}) => { +const InteractiveMark = ({children, nested}: MarkProps) => { const mark = useMark() const [isHighlighted, setIsHighlighted] = useState(false) @@ -186,9 +187,9 @@ const InteractiveMark = ({children, nested}: {value?: string; children?: ReactNo ) } -export const InteractiveNested: Story = { +export const InteractiveNested: StoryObj> = { args: { - Mark: InteractiveMark as ComponentType, + Mark: InteractiveMark, value: '@[Click me @[or me @[or even me]]]', options: [{markup: '@[__nested__]'}], }, @@ -198,15 +199,13 @@ export const InteractiveNested: Story = { // Example 6: Complex Markdown Document // ============================================================================ -const MarkdownMark = ({ - children, - value, - style, -}: { - value?: string - children?: ReactNode - style?: React.CSSProperties -}) => {children || value} +interface MarkdownMarkProps extends MarkProps { + style?: CSSProperties +} + +const MarkdownMark = ({children, value, style}: MarkdownMarkProps) => ( + {children || value} +) export const ComplexMarkdown: Story = { args: { @@ -236,7 +235,7 @@ export const ComplexMarkdown: Story = { // Example 7: Complex HTML Document // ============================================================================ -const HtmlDocMark = ({children, value, nested}: {value?: string; children?: ReactNode; nested?: string}) => { +const HtmlDocMark = ({children, value, nested}: MarkProps) => { const tagName = value?.toLowerCase() || 'span' const tagStyles: Record = { diff --git a/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx b/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx index 5f540480..66f2d97d 100644 --- a/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx +++ b/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx @@ -1,9 +1,10 @@ -import type {MarkProps, Markup} from '@markput/react' +import type {MarkProps, MarkedInputProps, Markup} from '@markput/react' import {MarkedInput, useOverlay} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' import type {ComponentType} from 'react' import {useEffect, useState} from 'react' import {Input, Popover, Tag} from 'rsuite' +import type {TagProps} from 'rsuite' import {Text} from '../../shared/components/Text' import {withStyle} from '../../shared/lib/withStyle' @@ -14,8 +15,6 @@ export default { decorators: [withStyle('rsuite.min.css')], } as Meta -type Story = StoryObj> - const Overlay = () => { const {style, match, select, close} = useOverlay() @@ -70,9 +69,9 @@ export const Overridden = () => { ) } -export const TaggedInput: Story = { +export const TaggedInput: StoryObj> = { args: { - Mark: Tag as ComponentType, + Mark: Tag as ComponentType, Overlay, value: initialState, className: 'rs-picker-tag-wrapper rs-picker-input rs-picker-toggle-wrapper rs-picker-tag', diff --git a/packages/react/storybook/src/pages/Slots/Slots.stories.tsx b/packages/react/storybook/src/pages/Slots/Slots.stories.tsx index 99c5deb8..dec83bb5 100644 --- a/packages/react/storybook/src/pages/Slots/Slots.stories.tsx +++ b/packages/react/storybook/src/pages/Slots/Slots.stories.tsx @@ -1,6 +1,6 @@ +import type {MarkProps, MarkedInputProps} from '@markput/react' import {MarkedInput} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' -import type {ComponentType} from 'react' import {useState} from 'react' const meta = { @@ -18,9 +18,8 @@ const meta = { } satisfies Meta export default meta -type Story = StoryObj -const SimpleMark = ({children}: {value?: string; children?: React.ReactNode}) => ( +const SimpleMark = ({children}: MarkProps) => ( {children} ) @@ -56,9 +55,9 @@ const FancySpan = ({ref, ...props}: React.HTMLAttributes & {ref * Using slots to completely replace container and span components. * This is useful when you need full control over the component structure. */ -export const CustomComponents: Story = { +export const CustomComponents: StoryObj> = { args: { - Mark: SimpleMark as ComponentType, + Mark: SimpleMark, value: 'Both @[container] and @[span] are @[customized]', slots: { container: FancyContainer, @@ -78,9 +77,9 @@ export const CustomComponents: Story = { * Using slotProps to customize styling and add event handlers. * This is useful when you want to keep the default components but customize their behavior. */ -export const WithSlotProps: Story = { +export const WithSlotProps: StoryObj> = { args: { - Mark: SimpleMark as ComponentType, + Mark: SimpleMark, value: 'Try pressing @[Enter] or clicking', className: 'custom-container', slotProps: { @@ -163,9 +162,9 @@ const StyledContainer = ({ref, ...props}: React.HTMLAttributes & * Edge case: Style merging when both slots and slotProps provide styles. * Shows that slotProps styles take precedence. */ -export const StyleMerging: Story = { +export const StyleMerging: StoryObj> = { args: { - Mark: SimpleMark as ComponentType, + Mark: SimpleMark, value: 'Container has @[merged] styles from multiple sources', slots: { container: StyledContainer, @@ -194,9 +193,9 @@ export const StyleMerging: Story = { * Data attributes support using camelCase notation. * React automatically converts camelCase properties like 'dataUserId' to HTML attributes like 'data-user-id'. */ -export const DataAttributes: Story = { +export const DataAttributes: StoryObj> = { args: { - Mark: SimpleMark as ComponentType, + Mark: SimpleMark, value: 'Use @[data] attributes for testing and tracking', slotProps: { container: { From 79ade79a0076b2cbe3063dd90e09344f5608101d Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 15 Mar 2026 15:29:27 +0300 Subject: [PATCH 04/11] refactor(storybook): streamline Drag stories and enhance args usage - Updated Drag stories to utilize args for props, improving readability and maintainability. - Removed unnecessary state management in favor of args, ensuring a more consistent approach across stories. - Enhanced the `withPlainValue` decorator to better manage controlled and uncontrolled stories. - Improved overall structure and organization of story files for clarity. --- .../storybook/src/pages/Drag/Drag.stories.tsx | 51 +++++++++++-------- .../src/pages/Dynamic/Dynamic.stories.tsx | 4 +- .../src/shared/lib/withPlainValue.tsx | 20 +++++--- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/packages/react/storybook/src/pages/Drag/Drag.stories.tsx b/packages/react/storybook/src/pages/Drag/Drag.stories.tsx index 599b4869..f9915cb8 100644 --- a/packages/react/storybook/src/pages/Drag/Drag.stories.tsx +++ b/packages/react/storybook/src/pages/Drag/Drag.stories.tsx @@ -180,14 +180,17 @@ const TODO_VALUE = `# \u{1F4CB} Project Launch Checklist const testStyle: React.CSSProperties = {minHeight: 100, padding: 8, border: '1px solid #e0e0e0'} export const PlainTextDrag: Story = { - parameters: {docs: {disable: true}, plainValue: false}, - render: () => { - const [value, setValue] = useState( - 'First block of plain text\n\nSecond block of plain text\n\nThird block of plain text\n\nFourth block of plain text\n\nFifth block of plain text' - ) + parameters: {docs: {disable: true}}, + args: { + value: 'First block of plain text\n\nSecond block of plain text\n\nThird block of plain text\n\nFourth block of plain text\n\nFifth block of plain text', + drag: true, + style: testStyle, + }, + render: args => { + const [value, setValue] = useState(args.value as string) return ( <> - + ) @@ -195,21 +198,19 @@ export const PlainTextDrag: Story = { } export const MarkdownDrag: StoryObj> = { - parameters: {docs: {disable: true}, plainValue: false}, - render: () => { - const [value, setValue] = useState( - '# Welcome to Draggable Blocks\n\nThis is the first paragraph.\n\nThis is the second paragraph.\n\n## Features\n\n- Drag handles appear on hover' - ) + parameters: {docs: {disable: true}}, + args: { + Mark: MarkdownMark, + options: markdownOptions, + value: '# Welcome to Draggable Blocks\n\nThis is the first paragraph.\n\nThis is the second paragraph.\n\n## Features\n\n- Drag handles appear on hover', + drag: true, + style: testStyle, + }, + render: args => { + const [value, setValue] = useState(args.value as string) return ( <> - + ) @@ -217,8 +218,13 @@ export const MarkdownDrag: StoryObj> = { } export const ReadOnlyDrag: Story = { - parameters: {docs: {disable: true}, plainValue: false}, - render: () => , + parameters: {docs: {disable: true}}, + args: { + value: 'Read-Only Content\n\nSection A\n\nSection B', + readOnly: true, + drag: true, + style: testStyle, + }, } // ─── Todo list (all marks include \n\n) ─────────────────────────────────────── @@ -230,4 +236,7 @@ export const TodoList: StoryObj> = { value: TODO_VALUE, drag: true, }, + parameters: { + plainValue: true, + }, } \ No newline at end of file diff --git a/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx b/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx index bed98981..79759dbc 100644 --- a/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx +++ b/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx @@ -35,7 +35,7 @@ const RemovableMark = () => { } export const Removable: Story = { - parameters: {docs: {disable: true}, plainValue: false}, + parameters: {docs: {disable: true}}, args: { Mark: RemovableMark, value: 'I @[contain]( ) @[removable]( ) by click @[marks]( )!', @@ -63,7 +63,7 @@ const Abbr = () => { } export const Focusable: Story = { - parameters: {docs: {disable: true}}, + parameters: {docs: {disable: true}, plainValue: true}, args: { Mark: Abbr, value: 'Hello, @[focusable](By key operations) abbreviation @[world](Hello! Hello!)!', diff --git a/packages/react/storybook/src/shared/lib/withPlainValue.tsx b/packages/react/storybook/src/shared/lib/withPlainValue.tsx index 6d925741..fb9b9b2b 100644 --- a/packages/react/storybook/src/shared/lib/withPlainValue.tsx +++ b/packages/react/storybook/src/shared/lib/withPlainValue.tsx @@ -4,17 +4,21 @@ import {useArgs, useGlobals} from 'storybook/preview-api' import {Text} from '../components/Text' export const withPlainValue = (Story: any, context: any) => { - if (context.parameters?.plainValue === false) { - return - } + // Hooks must be called unconditionally (no early return before them). const [args, updateArgs] = useArgs() const [globals] = useGlobals() - const isControlled = 'value' in args + + // In test environments useArgs() may return {} on the first render. + // Merge context.args (initial story args) with the reactive args from useArgs(). + const mergedArgs = {...context.args, ...args} + + const isControlled = 'value' in mergedArgs + const showPanel = context.parameters?.plainValue === true const showPlainValue = globals.showPlainValue !== 'hide' // displayValue tracks onChange synchronously so stays up-to-date in // tests where updateArgs propagation is async. - const [displayValue, setDisplayValue] = useState(args.value) + const [displayValue, setDisplayValue] = useState((context.args as any)?.value) const handleChange = useCallback( (newValue: string) => { @@ -24,14 +28,14 @@ export const withPlainValue = (Story: any, context: any) => { [updateArgs] ) - // Only wrap controlled stories (those with `value` in args). + // Only wrap controlled stories that opted in to the panel. // Uncontrolled stories use `defaultValue` — overriding onChange would inject // `value` back via updateArgs, switching them to controlled mode. - if (!isControlled) { + if (!showPanel || !isControlled) { return } - const storyArgs = {...args, onChange: handleChange} + const storyArgs = {...mergedArgs, onChange: handleChange} if (!showPlainValue) { return From f76c25cba862a5892a199a6e2dffae859b231b9f Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 15 Mar 2026 16:09:30 +0300 Subject: [PATCH 05/11] refactor(storybook): update Text component to PlainValuePanel with enhanced styling and functionality - Refactored the Text component into PlainValuePanel, improving structure and readability. - Introduced new CSS styles for the PlainValuePanel, including responsive design for positioning. - Added a copy button with feedback and a statistics footer displaying word, character, and line counts. - Updated the withPlainValue decorator to support dynamic positioning based on container width. --- .../storybook/src/pages/Drag/Drag.stories.tsx | 17 +- .../src/shared/components/Text/Text.css | 146 +++++++++++------- .../src/shared/components/Text/Text.tsx | 47 ++++-- .../src/shared/lib/withPlainValue.tsx | 56 ++++--- 4 files changed, 166 insertions(+), 100 deletions(-) diff --git a/packages/react/storybook/src/pages/Drag/Drag.stories.tsx b/packages/react/storybook/src/pages/Drag/Drag.stories.tsx index f9915cb8..0e29a4e6 100644 --- a/packages/react/storybook/src/pages/Drag/Drag.stories.tsx +++ b/packages/react/storybook/src/pages/Drag/Drag.stories.tsx @@ -4,7 +4,6 @@ import type {Meta, StoryObj} from '@storybook/react-vite' import type {CSSProperties} from 'react' import {useState} from 'react' -import {Text} from '../../shared/components/Text' import {DRAG_MARKDOWN} from '../../shared/lib/sampleTexts' import {markdownOptions} from '../Nested/MarkdownOptions' @@ -188,12 +187,7 @@ export const PlainTextDrag: Story = { }, render: args => { const [value, setValue] = useState(args.value as string) - return ( - <> - - - - ) + return }, } @@ -208,12 +202,7 @@ export const MarkdownDrag: StoryObj> = { }, render: args => { const [value, setValue] = useState(args.value as string) - return ( - <> - - - - ) + return }, } @@ -237,6 +226,6 @@ export const TodoList: StoryObj> = { drag: true, }, parameters: { - plainValue: true, + plainValue: 'right', }, } \ No newline at end of file diff --git a/packages/react/storybook/src/shared/components/Text/Text.css b/packages/react/storybook/src/shared/components/Text/Text.css index b110eae8..65884d70 100644 --- a/packages/react/storybook/src/shared/components/Text/Text.css +++ b/packages/react/storybook/src/shared/components/Text/Text.css @@ -1,79 +1,115 @@ -.text-container { - border: 1px solid #d0d7de; - border-radius: 6px; +/* ============================================================ + PlainValuePanel styles (pvp-*) + ============================================================ */ + +/* --- Dashed divider for bottom mode --- */ +.pvp-divider { + border: none; + border-top: 1.5px dashed #d0d7de; + margin: 16px 0 0; +} + +/* --- Outer container --- */ +.pvp-container { + display: flex; + flex-direction: column; +} + +/* Right mode: proportional flex column panel, full height */ +.pvp-right { + flex: 2; + min-width: 280px; + border-left: 1px solid #e5e7eb; + height: 100%; overflow: hidden; } -.text-header { - background: #f0f3f6; - padding: 6px 16px; - border-bottom: 1px solid #d0d7de; +/* Bottom mode: full width, stacked below editor */ +.pvp-bottom { + width: 100%; + border-top: 1px solid #e5e7eb; } -.text-label { - font-size: 12px; +/* --- Header row --- */ +.pvp-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + flex-shrink: 0; + border-bottom: 1px solid #e5e7eb; +} + +.pvp-label { + font-size: 10px; font-weight: 600; - color: #6b7280; text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.5px; + color: #999; + white-space: nowrap; font-family: Arial, Helvetica, sans-serif; } -.text-label::before { +.pvp-label::before { content: ' '; - color: #0550ae; - font-style: normal; + color: #6b7280; letter-spacing: 0; } -.text-container.text-inset { - border: none; - border-left: 1px solid #d0d7de; - border-radius: 0; - height: 100%; - background: #f6f8fa; -} - -.text-container.text-inset .text-header { +/* --- Copy button --- */ +.pvp-copy { + margin-left: auto; + font-size: 10px; + padding: 2px 8px; + border: 1px solid #d1d5db; + border-radius: 4px; background: transparent; - border-bottom-color: #d0d7de; + cursor: pointer; + color: #6b7280; + white-space: nowrap; + font-family: Arial, Helvetica, sans-serif; + line-height: 1.6; } -.text-container.text-inset .text-pre { - background: transparent; +.pvp-copy:hover { + background: #f3f4f6; } -.tok-heading-prefix { - color: #8b949e; -} -.tok-heading-text { - color: #0550ae; - font-weight: 600; -} -.tok-bullet { - color: #8b949e; -} -.tok-checked { - color: #1a7f37; - font-weight: 600; -} -.tok-unchecked { - color: #8b949e; -} -.tok-blockquote { - color: #8b949e; - font-style: italic; +/* --- Scrollable content area --- */ +.pvp-scroll { + flex: 1; + overflow-y: auto; + min-height: 0; } -.text-pre { +/* --- Monospace text directly on white surface --- */ +.pvp-pre { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; - font-size: 13px; - background: #f6f8fa; - padding: 12px 12px; - overflow-x: auto; - line-height: 1.5; - color: #24292f; + font-size: 11px; + line-height: 1.6; + tab-size: 2; + background: transparent; + color: #374151; + padding: 12px 14px; margin: 0; - white-space: pre-wrap; - word-break: break-word; + border: none; + border-radius: 0; + white-space: pre; + overflow-x: auto; +} + +/* --- Stats footer --- */ +.pvp-footer { + font-size: 10px; + color: #9ca3af; + padding: 5px 12px 6px; + border-top: 1px solid #d1d5db; + flex-shrink: 0; + font-family: Arial, Helvetica, sans-serif; +} + +/* --- Empty placeholder --- */ +.pvp-empty { + color: #9ca3af; + font-style: italic; } diff --git a/packages/react/storybook/src/shared/components/Text/Text.tsx b/packages/react/storybook/src/shared/components/Text/Text.tsx index 8ee03e58..43e93c43 100644 --- a/packages/react/storybook/src/shared/components/Text/Text.tsx +++ b/packages/react/storybook/src/shared/components/Text/Text.tsx @@ -1,18 +1,41 @@ +import {useState} from 'react' + import './Text.css' -export interface TextProps { +function computeStats(value: string): string { + if (!value) return '0 words · 0 chars · 0 lines' + const words = value.trim().split(/\s+/).filter(Boolean).length + const chars = value.length + const lines = value.split('\n').length + return `${words} words · ${chars} chars · ${lines} lines` +} + +interface PlainValuePanelProps { value: string - label?: string - className?: string + position: 'right' | 'bottom' } -export const Text = ({value, label, className}: TextProps) => ( -
- {label && ( -
- {label} +export const PlainValuePanel = ({value, position}: PlainValuePanelProps) => { + const [copied, setCopied] = useState(false) + + const handleCopy = () => { + navigator.clipboard.writeText(value) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } + + return ( +
+
+ PLAIN VALUE + +
+
+
{value || (empty)}
- )} -
{value}
-
-) \ No newline at end of file +
{computeStats(value)}
+
+ ) +} \ No newline at end of file diff --git a/packages/react/storybook/src/shared/lib/withPlainValue.tsx b/packages/react/storybook/src/shared/lib/withPlainValue.tsx index fb9b9b2b..7d8cac3c 100644 --- a/packages/react/storybook/src/shared/lib/withPlainValue.tsx +++ b/packages/react/storybook/src/shared/lib/withPlainValue.tsx @@ -1,7 +1,7 @@ -import {useCallback, useState} from 'react' +import {useCallback, useEffect, useRef, useState} from 'react' import {useArgs, useGlobals} from 'storybook/preview-api' -import {Text} from '../components/Text' +import {PlainValuePanel} from '../components/Text' export const withPlainValue = (Story: any, context: any) => { // Hooks must be called unconditionally (no early return before them). @@ -13,13 +13,29 @@ export const withPlainValue = (Story: any, context: any) => { const mergedArgs = {...context.args, ...args} const isControlled = 'value' in mergedArgs - const showPanel = context.parameters?.plainValue === true + const rawPosition = context.parameters?.plainValue as 'right' | 'bottom' | undefined + const showPanel = rawPosition === 'right' || rawPosition === 'bottom' const showPlainValue = globals.showPlainValue !== 'hide' - // displayValue tracks onChange synchronously so stays up-to-date in + // displayValue tracks onChange synchronously so the panel stays up-to-date in // tests where updateArgs propagation is async. const [displayValue, setDisplayValue] = useState((context.args as any)?.value) + // Responsive: switch to 'bottom' when wrapper is narrower than 600px. + const wrapperRef = useRef(null) + const [effectivePosition, setEffectivePosition] = useState<'right' | 'bottom'>(rawPosition ?? 'right') + + useEffect(() => { + const el = wrapperRef.current + if (!el || !rawPosition) return + const observer = new ResizeObserver(([entry]) => { + const width = entry.contentRect.width + setEffectivePosition(width < 600 ? 'bottom' : rawPosition) + }) + observer.observe(el) + return () => observer.disconnect() + }, [rawPosition]) + const handleChange = useCallback( (newValue: string) => { setDisplayValue(newValue) @@ -41,23 +57,25 @@ export const withPlainValue = (Story: any, context: any) => { return } - return ( -
-
- + if (effectivePosition === 'right') { + return ( +
+
+ +
+ {displayValue !== undefined && }
+ ) + } + + return ( +
+ {displayValue !== undefined && ( -
- -
+ <> +
+ + )}
) From e7c82391b4ed12fc6886780c02843c84db73b815 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 15 Mar 2026 16:29:13 +0300 Subject: [PATCH 06/11] refactor(storybook): update Plain Value panel configuration and styling - Changed the default position of the Plain Value panel to 'right' and updated toolbar items for better clarity. - Removed unnecessary CSS for the dashed divider in the Text component. - Enhanced the `withPlainValue` decorator to manage global positioning and visibility more effectively, improving responsiveness based on container width. --- packages/react/storybook/.storybook/preview.ts | 11 ++++++----- .../storybook/src/shared/components/Text/Text.css | 1 - .../storybook/src/shared/lib/withPlainValue.tsx | 12 +++++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/react/storybook/.storybook/preview.ts b/packages/react/storybook/.storybook/preview.ts index 45b8b16e..ddb66c78 100644 --- a/packages/react/storybook/.storybook/preview.ts +++ b/packages/react/storybook/.storybook/preview.ts @@ -7,13 +7,14 @@ const preview: Preview = { globalTypes: { showPlainValue: { name: 'Plain Value', - description: 'Toggle plain value panel', - defaultValue: 'show', + description: 'Plain value panel position', + defaultValue: 'right', toolbar: { - icon: 'eye', + icon: 'sidebaralt', items: [ - {value: 'show', icon: 'eye'}, - {value: 'hide', icon: 'eyeclose'}, + {value: 'right', title: 'Show right', icon: 'sidebaralt'}, + {value: 'bottom', title: 'Show bottom', icon: 'bottombar'}, + {value: 'hide', title: 'Hide', icon: 'eyeclose'}, ], }, }, diff --git a/packages/react/storybook/src/shared/components/Text/Text.css b/packages/react/storybook/src/shared/components/Text/Text.css index 65884d70..5155d018 100644 --- a/packages/react/storybook/src/shared/components/Text/Text.css +++ b/packages/react/storybook/src/shared/components/Text/Text.css @@ -5,7 +5,6 @@ /* --- Dashed divider for bottom mode --- */ .pvp-divider { border: none; - border-top: 1.5px dashed #d0d7de; margin: 16px 0 0; } diff --git a/packages/react/storybook/src/shared/lib/withPlainValue.tsx b/packages/react/storybook/src/shared/lib/withPlainValue.tsx index 7d8cac3c..a296fec7 100644 --- a/packages/react/storybook/src/shared/lib/withPlainValue.tsx +++ b/packages/react/storybook/src/shared/lib/withPlainValue.tsx @@ -15,7 +15,9 @@ export const withPlainValue = (Story: any, context: any) => { const isControlled = 'value' in mergedArgs const rawPosition = context.parameters?.plainValue as 'right' | 'bottom' | undefined const showPanel = rawPosition === 'right' || rawPosition === 'bottom' - const showPlainValue = globals.showPlainValue !== 'hide' + const globalValue = (globals.showPlainValue ?? 'right') as 'right' | 'bottom' | 'hide' + const showPlainValue = globalValue !== 'hide' + const globalPosition: 'right' | 'bottom' = globalValue === 'hide' ? 'right' : globalValue // displayValue tracks onChange synchronously so the panel stays up-to-date in // tests where updateArgs propagation is async. @@ -23,18 +25,18 @@ export const withPlainValue = (Story: any, context: any) => { // Responsive: switch to 'bottom' when wrapper is narrower than 600px. const wrapperRef = useRef(null) - const [effectivePosition, setEffectivePosition] = useState<'right' | 'bottom'>(rawPosition ?? 'right') + const [effectivePosition, setEffectivePosition] = useState<'right' | 'bottom'>(globalPosition) useEffect(() => { const el = wrapperRef.current - if (!el || !rawPosition) return + if (!el || !showPanel) return const observer = new ResizeObserver(([entry]) => { const width = entry.contentRect.width - setEffectivePosition(width < 600 ? 'bottom' : rawPosition) + setEffectivePosition(width < 600 ? 'bottom' : globalPosition) }) observer.observe(el) return () => observer.disconnect() - }, [rawPosition]) + }, [globalPosition, showPlainValue, showPanel]) const handleChange = useCallback( (newValue: string) => { From 145fcef324a6a91c5154029496616969ec04c990 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 15 Mar 2026 16:29:56 +0300 Subject: [PATCH 07/11] refactor(storybook): simplify story components by removing unnecessary state management - Removed state management and Text component usage from several stories, streamlining the render functions. - Updated the Focusable story to change the plainValue parameter from true to 'right'. - Enhanced the overall structure and readability of the story files by focusing on essential components. --- .../storybook/src/pages/Base/Base.stories.tsx | 42 ++++++-------- .../src/pages/Dynamic/Dynamic.stories.tsx | 2 +- .../Experimental/Experimental.stories.tsx | 39 ++++--------- .../src/pages/Material/Material.stories.tsx | 58 ++++++++----------- .../src/pages/Rsuite/Rsuite.stories.tsx | 33 +++++------ 5 files changed, 65 insertions(+), 109 deletions(-) diff --git a/packages/react/storybook/src/pages/Base/Base.stories.tsx b/packages/react/storybook/src/pages/Base/Base.stories.tsx index b86d7d06..a00ca3fe 100644 --- a/packages/react/storybook/src/pages/Base/Base.stories.tsx +++ b/packages/react/storybook/src/pages/Base/Base.stories.tsx @@ -1,10 +1,9 @@ -import type {MarkProps, MarkToken, Markup} from '@markput/react' -import {denote, MarkedInput} from '@markput/react' +import type {MarkProps, Markup} from '@markput/react' +import {MarkedInput} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' import {useState} from 'react' import {Button} from '../../shared/components/Button' -import {Text} from '../../shared/components/Text' export default { title: 'MarkedInput', @@ -52,29 +51,22 @@ export const Configured: Story = { 'For found mark used @[annotations](default:123).' ) - const displayText = denote(value, (mark: MarkToken) => mark.value, [PrimaryMarkup, DefaultMarkup]) - return ( - <> - console.log('onCLick'), - onInput: _ => console.log('onInput'), - onBlur: _ => console.log('onBlur'), - onFocus: _ => console.log('onFocus'), - onKeyDown: _ => console.log('onKeyDown'), - }, - }} - /> - - - - + console.log('onCLick'), + onInput: _ => console.log('onInput'), + onBlur: _ => console.log('onBlur'), + onFocus: _ => console.log('onFocus'), + onKeyDown: _ => console.log('onKeyDown'), + }, + }} + /> ) }, } diff --git a/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx b/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx index 79759dbc..58599e8e 100644 --- a/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx +++ b/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx @@ -63,7 +63,7 @@ const Abbr = () => { } export const Focusable: Story = { - parameters: {docs: {disable: true}, plainValue: true}, + parameters: {docs: {disable: true}, plainValue: 'right'}, args: { Mark: Abbr, value: 'Hello, @[focusable](By key operations) abbreviation @[world](Hello! Hello!)!', diff --git a/packages/react/storybook/src/pages/Experimental/Experimental.stories.tsx b/packages/react/storybook/src/pages/Experimental/Experimental.stories.tsx index cf99bff1..244706e3 100644 --- a/packages/react/storybook/src/pages/Experimental/Experimental.stories.tsx +++ b/packages/react/storybook/src/pages/Experimental/Experimental.stories.tsx @@ -1,8 +1,6 @@ import {MarkedInput} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' -import {useState} from 'react' -import {Text} from '../../shared/components/Text' import {SingleEditableControlled} from './components/SingleEditableControlled' import {SingleEditableMarkdown} from './components/SingleEditableMarkdown' import {SingleEditableUncontrolled} from './components/SingleEditableUncontrolled' @@ -30,21 +28,14 @@ type Story = StoryObj> * This story shows WHY we need the uncontrolled approach. */ export const Controlled: Story = { - render: () => { - const [value, setValue] = useState('') - - return ( - <> -
-

❌ Controlled (React-managed)

-
- - - - - - ) - }, + render: () => ( + <> +
+

❌ Controlled (React-managed)

+
+ {}} /> + + ), } /** @@ -60,17 +51,12 @@ export const Controlled: Story = { */ export const Uncontrolled: Story = { render: () => { - const [value, setValue] = useState('') - return ( <>

✅ Uncontrolled (MutationObserver)

- - - - + {}} /> ) }, @@ -91,17 +77,12 @@ export const Uncontrolled: Story = { */ export const Markdown: Story = { render: () => { - const [value, setValue] = useState('') - return ( <>

📝 Markdown (Uncontrolled)

- - - - + {}} /> ) }, diff --git a/packages/react/storybook/src/pages/Material/Material.stories.tsx b/packages/react/storybook/src/pages/Material/Material.stories.tsx index 1ea6c263..bb1dc025 100644 --- a/packages/react/storybook/src/pages/Material/Material.stories.tsx +++ b/packages/react/storybook/src/pages/Material/Material.stories.tsx @@ -6,7 +6,6 @@ import type {Meta, StoryObj} from '@storybook/react-vite' import type {ComponentType} from 'react' import {useState} from 'react' -import {Text} from '../../shared/components/Text' import {MaterialMentions} from './components/MaterialMentions' export default { @@ -17,13 +16,7 @@ export default { export const Mentions = () => { const [value, setValue] = useState(`Enter the '@' for calling mention list: \n- Hello @Agustina and @[Ruslan]!`) - return ( - <> - - - - - ) + return } const initialValue = 'Hello beautiful the @[first](outlined:1) world from the @[second](common:2) ' @@ -49,32 +42,27 @@ export const Overridden = () => { const [value, setValue] = useState(initialValue) return ( - <> - ({ - label: value, - variant: 'outlined' as const, - size: 'small' as const, - }), - }, - { - markup: '@[__value__](common:__meta__)', - mark: ({value}: MarkProps) => ({label: value, size: 'small' as const}), - }, - ], - }} - value={value} - onChange={setValue as any} - /> - -
- - + ({ + label: value, + variant: 'outlined' as const, + size: 'small' as const, + }), + }, + { + markup: '@[__value__](common:__meta__)', + mark: ({value}: MarkProps) => ({label: value, size: 'small' as const}), + }, + ], + }} + value={value} + onChange={setValue as any} + /> ) } \ No newline at end of file diff --git a/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx b/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx index 66f2d97d..2670dfc0 100644 --- a/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx +++ b/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx @@ -6,7 +6,6 @@ import {useEffect, useState} from 'react' import {Input, Popover, Tag} from 'rsuite' import type {TagProps} from 'rsuite' -import {Text} from '../../shared/components/Text' import {withStyle} from '../../shared/lib/withStyle' export default { @@ -48,24 +47,20 @@ export const Overridden = () => { const [value, setValue] = useState(initialState) return ( - <> - setValue(value as unknown as string)} - options={[ - { - markup: '@[__value__](common)' as Markup, - mark: ({value}: {value?: string}) => ({children: value}), - overlay: {trigger: '@'}, - }, - ]} - /> - - - + setValue(value as unknown as string)} + options={[ + { + markup: '@[__value__](common)' as Markup, + mark: ({value}: {value?: string}) => ({children: value}), + overlay: {trigger: '@'}, + }, + ]} + /> ) } From c040d8af0bd0b9e32954f32dfbf9fab57201d5c4 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 15 Mar 2026 23:08:48 +0300 Subject: [PATCH 08/11] refactor(storybook): update PlainValuePanel integration and streamline Drag stories - Modified the PlainValuePanel to include a data-value attribute for better data handling. - Updated Drag stories to utilize the new plainValue parameter for positioning. - Removed unnecessary state management in story components, enhancing clarity and maintainability. - Improved the overall structure of the withPlainValue decorator for better responsiveness and functionality. --- .../storybook/src/pages/Drag/Drag.spec.tsx | 4 +- .../storybook/src/pages/Drag/Drag.stories.tsx | 12 +- .../src/shared/components/Text/Text.tsx | 4 +- .../src/shared/lib/withPlainValue.tsx | 115 +++++++++++------- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/packages/react/storybook/src/pages/Drag/Drag.spec.tsx b/packages/react/storybook/src/pages/Drag/Drag.spec.tsx index 6bd545c2..c1e60160 100644 --- a/packages/react/storybook/src/pages/Drag/Drag.spec.tsx +++ b/packages/react/storybook/src/pages/Drag/Drag.spec.tsx @@ -26,9 +26,9 @@ function getBlocks(container: Element) { return Array.from(container.querySelectorAll('[data-testid="block"]')) } -/** Read the raw value from the
 rendered by the Text component */
+/** Read the raw value from the PlainValuePanel's data-value attribute */
 function getRawValue(container: Element) {
-	return container.querySelector('pre')!.textContent!
+	return container.querySelector('pre[data-value]')!.dataset.value!
 }
 
 /**
diff --git a/packages/react/storybook/src/pages/Drag/Drag.stories.tsx b/packages/react/storybook/src/pages/Drag/Drag.stories.tsx
index 0e29a4e6..a65da1dd 100644
--- a/packages/react/storybook/src/pages/Drag/Drag.stories.tsx
+++ b/packages/react/storybook/src/pages/Drag/Drag.stories.tsx
@@ -179,20 +179,16 @@ const TODO_VALUE = `# \u{1F4CB} Project Launch Checklist
 const testStyle: React.CSSProperties = {minHeight: 100, padding: 8, border: '1px solid #e0e0e0'}
 
 export const PlainTextDrag: Story = {
-	parameters: {docs: {disable: true}},
+	parameters: {docs: {disable: true}, plainValue: 'bottom'},
 	args: {
 		value: 'First block of plain text\n\nSecond block of plain text\n\nThird block of plain text\n\nFourth block of plain text\n\nFifth block of plain text',
 		drag: true,
 		style: testStyle,
 	},
-	render: args => {
-		const [value, setValue] = useState(args.value as string)
-		return 
-	},
 }
 
 export const MarkdownDrag: StoryObj> = {
-	parameters: {docs: {disable: true}},
+	parameters: {docs: {disable: true}, plainValue: 'bottom'},
 	args: {
 		Mark: MarkdownMark,
 		options: markdownOptions,
@@ -200,10 +196,6 @@ export const MarkdownDrag: StoryObj> = {
 		drag: true,
 		style: testStyle,
 	},
-	render: args => {
-		const [value, setValue] = useState(args.value as string)
-		return 
-	},
 }
 
 export const ReadOnlyDrag: Story = {
diff --git a/packages/react/storybook/src/shared/components/Text/Text.tsx b/packages/react/storybook/src/shared/components/Text/Text.tsx
index 43e93c43..e9795c3a 100644
--- a/packages/react/storybook/src/shared/components/Text/Text.tsx
+++ b/packages/react/storybook/src/shared/components/Text/Text.tsx
@@ -33,7 +33,9 @@ export const PlainValuePanel = ({value, position}: PlainValuePanelProps) => {
 				
 			
-
{value || (empty)}
+
+					{value || (empty)}
+				
{computeStats(value)}
diff --git a/packages/react/storybook/src/shared/lib/withPlainValue.tsx b/packages/react/storybook/src/shared/lib/withPlainValue.tsx index a296fec7..c11aa241 100644 --- a/packages/react/storybook/src/shared/lib/withPlainValue.tsx +++ b/packages/react/storybook/src/shared/lib/withPlainValue.tsx @@ -3,69 +3,64 @@ import {useArgs, useGlobals} from 'storybook/preview-api' import {PlainValuePanel} from '../components/Text' -export const withPlainValue = (Story: any, context: any) => { - // Hooks must be called unconditionally (no early return before them). - const [args, updateArgs] = useArgs() - const [globals] = useGlobals() +// ─── Proper React component that owns all local state ───────────────────────── +// withPlainValue (a Storybook decorator) is NOT called as a React component — +// it is invoked as a plain function inside Storybook's hookify wrapper. +// Calling React hooks (useState, useEffect) from that context can produce +// undefined initial state on the first render. +// PanelContainer IS a real React component: React creates a dedicated fiber +// for it, so useState always initialises correctly. - // In test environments useArgs() may return {} on the first render. - // Merge context.args (initial story args) with the reactive args from useArgs(). - const mergedArgs = {...context.args, ...args} +interface PanelContainerProps { + Story: any + args: any + value: string + position: 'right' | 'bottom' + updateArgs: (update: Record) => void +} - const isControlled = 'value' in mergedArgs - const rawPosition = context.parameters?.plainValue as 'right' | 'bottom' | undefined - const showPanel = rawPosition === 'right' || rawPosition === 'bottom' - const globalValue = (globals.showPlainValue ?? 'right') as 'right' | 'bottom' | 'hide' - const showPlainValue = globalValue !== 'hide' - const globalPosition: 'right' | 'bottom' = globalValue === 'hide' ? 'right' : globalValue +function PanelContainer({Story, args, value: valueProp, position: positionProp, updateArgs}: PanelContainerProps) { + const [value, setValue] = useState(valueProp) + const [prevValueProp, setPrevValueProp] = useState(valueProp) - // displayValue tracks onChange synchronously so the panel stays up-to-date in - // tests where updateArgs propagation is async. - const [displayValue, setDisplayValue] = useState((context.args as any)?.value) + // Sync when the external value changes (e.g. Storybook controls panel). + // getDerivedStateFromProps pattern: call setState during render when prop drifts. + if (valueProp !== prevValueProp) { + setPrevValueProp(valueProp) + setValue(valueProp) + } - // Responsive: switch to 'bottom' when wrapper is narrower than 600px. const wrapperRef = useRef(null) - const [effectivePosition, setEffectivePosition] = useState<'right' | 'bottom'>(globalPosition) + const [position, setPosition] = useState<'right' | 'bottom'>(positionProp) + // Responsive: switch to 'bottom' when wrapper is narrower than 600px. useEffect(() => { const el = wrapperRef.current - if (!el || !showPanel) return + if (!el) return const observer = new ResizeObserver(([entry]) => { - const width = entry.contentRect.width - setEffectivePosition(width < 600 ? 'bottom' : globalPosition) + setPosition(entry.contentRect.width < 600 ? 'bottom' : positionProp) }) observer.observe(el) return () => observer.disconnect() - }, [globalPosition, showPlainValue, showPanel]) + }, [positionProp]) const handleChange = useCallback( (newValue: string) => { - setDisplayValue(newValue) + setValue(newValue) updateArgs({value: newValue}) }, [updateArgs] ) - // Only wrap controlled stories that opted in to the panel. - // Uncontrolled stories use `defaultValue` — overriding onChange would inject - // `value` back via updateArgs, switching them to controlled mode. - if (!showPanel || !isControlled) { - return - } - - const storyArgs = {...mergedArgs, onChange: handleChange} + const storyArgs = {...args, value, onChange: handleChange} - if (!showPlainValue) { - return - } - - if (effectivePosition === 'right') { + if (position === 'right') { return (
- {displayValue !== undefined && } +
) } @@ -73,12 +68,46 @@ export const withPlainValue = (Story: any, context: any) => { return (
- {displayValue !== undefined && ( - <> -
- - - )} +
+
) +} + +// ─── Global Storybook decorator ─────────────────────────────────────────────── + +export const withPlainValue = (Story: any, context: any) => { + // Only Storybook hooks at this level — no React hooks. + const [args, updateArgs] = useArgs() + const [globals] = useGlobals() + + const mergedArgs = {...context.args, ...args} + const isControlled = 'value' in mergedArgs + const rawPosition = context.parameters?.plainValue as 'right' | 'bottom' | undefined + const showPanel = rawPosition === 'right' || rawPosition === 'bottom' + const globalValue = (globals.showPlainValue ?? 'right') as 'right' | 'bottom' | 'hide' + const showPlainValue = globalValue !== 'hide' + const globalPosition: 'right' | 'bottom' = globalValue === 'hide' ? 'right' : globalValue + + // Stories that don't opt in to the panel, or are uncontrolled. + if (!showPanel || !isControlled) { + return + } + + // Panel opted in but globally hidden — still wire onChange so controls stay in sync. + if (!showPlainValue) { + return updateArgs({value: v})}} /> + } + + const position = rawPosition ?? globalPosition + + return ( + + ) } \ No newline at end of file From 8a0e504fc85ec0332bfa4953681f33c9a75be886 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 15 Mar 2026 23:32:27 +0300 Subject: [PATCH 09/11] refactor(storybook): enhance story components and streamline rendering - Updated `Base.stories.tsx` to improve type safety with `MarkedInputProps` and added `ButtonProps` for better integration. - Refactored `Dynamic.stories.tsx` to utilize decorators for cleaner rendering logic. - Simplified rendering in `Experimental.stories.tsx` by removing unnecessary fragments. - Introduced `TabbedMarkdownView` and `TabbedHtmlView` components in `Nested.stories.tsx` for better code reuse and clarity. - Created `EventLogStory` in `Slots.stories.tsx` to manage event logging more effectively and improve user interaction tracking. --- .../storybook/src/pages/Base/Base.stories.tsx | 47 +++---- .../src/pages/Dynamic/Dynamic.stories.tsx | 10 +- .../Experimental/Experimental.stories.tsx | 31 +---- .../src/pages/Nested/Nested.stories.tsx | 88 +++++++------ .../src/pages/Slots/Slots.stories.tsx | 120 +++++++----------- 5 files changed, 120 insertions(+), 176 deletions(-) diff --git a/packages/react/storybook/src/pages/Base/Base.stories.tsx b/packages/react/storybook/src/pages/Base/Base.stories.tsx index a00ca3fe..9eca2b8e 100644 --- a/packages/react/storybook/src/pages/Base/Base.stories.tsx +++ b/packages/react/storybook/src/pages/Base/Base.stories.tsx @@ -1,9 +1,9 @@ -import type {MarkProps, Markup} from '@markput/react' +import type {MarkProps, MarkedInputProps, Markup, Option} from '@markput/react' import {MarkedInput} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' -import {useState} from 'react' import {Button} from '../../shared/components/Button' +import type {ButtonProps} from '../../shared/components/Button' export default { title: 'MarkedInput', @@ -24,7 +24,7 @@ export const Default: Story = { const PrimaryMarkup: Markup = '@[__value__](primary:__meta__)' const DefaultMarkup: Markup = '@[__value__](default:__meta__)' -const configuredOptions = [ +const configuredOptions: Option[] = [ { markup: PrimaryMarkup, mark: ({value, meta}: MarkProps) => ({label: value || '', primary: true, onClick: () => alert(meta)}), @@ -43,31 +43,24 @@ const configuredOptions = [ }, ] -export const Configured: Story = { - render: () => { - const [value, setValue] = useState( +export const Configured: StoryObj> = { + parameters: {plainValue: 'right'}, + args: { + Mark: Button, + options: configuredOptions, + value: "Enter the '@' for calling @[primary](primary:4) suggestions and '/' for @[default](default:7)!\n" + - 'Mark is can be a any component with any logic. In this example it is the @[Button](primary:54): clickable primary or secondary.\n' + - 'For found mark used @[annotations](default:123).' - ) - - return ( - console.log('onCLick'), - onInput: _ => console.log('onInput'), - onBlur: _ => console.log('onBlur'), - onFocus: _ => console.log('onFocus'), - onKeyDown: _ => console.log('onKeyDown'), - }, - }} - /> - ) + 'Mark is can be a any component with any logic. In this example it is the @[Button](primary:54): clickable primary or secondary.\n' + + 'For found mark used @[annotations](default:123).', + slotProps: { + container: { + onClick: _ => console.log('onCLick'), + onInput: _ => console.log('onInput'), + onBlur: _ => console.log('onBlur'), + onFocus: _ => console.log('onFocus'), + onKeyDown: _ => console.log('onKeyDown'), + }, + }, }, } diff --git a/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx b/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx index 58599e8e..2f27c4b1 100644 --- a/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx +++ b/packages/react/storybook/src/pages/Dynamic/Dynamic.stories.tsx @@ -68,10 +68,12 @@ export const Focusable: Story = { Mark: Abbr, value: 'Hello, @[focusable](By key operations) abbreviation @[world](Hello! Hello!)!', }, - render: args => { - useCaretInfo(true) - return - }, + decorators: [ + Story => { + useCaretInfo(true) + return + }, + ], } /*TODO diff --git a/packages/react/storybook/src/pages/Experimental/Experimental.stories.tsx b/packages/react/storybook/src/pages/Experimental/Experimental.stories.tsx index 244706e3..5d21ce77 100644 --- a/packages/react/storybook/src/pages/Experimental/Experimental.stories.tsx +++ b/packages/react/storybook/src/pages/Experimental/Experimental.stories.tsx @@ -28,14 +28,7 @@ type Story = StoryObj> * This story shows WHY we need the uncontrolled approach. */ export const Controlled: Story = { - render: () => ( - <> -
-

❌ Controlled (React-managed)

-
- {}} /> - - ), + render: () => {}} />, } /** @@ -50,16 +43,7 @@ export const Controlled: Story = { * This is the recommended approach for single contentEditable! */ export const Uncontrolled: Story = { - render: () => { - return ( - <> -
-

✅ Uncontrolled (MutationObserver)

-
- {}} /> - - ) - }, + render: () => {}} />, } /** @@ -76,14 +60,5 @@ export const Uncontrolled: Story = { * Uses the same uncontrolled approach with MutationObserver for smooth editing! */ export const Markdown: Story = { - render: () => { - return ( - <> -
-

📝 Markdown (Uncontrolled)

-
- {}} /> - - ) - }, + render: () => {}} />, } \ No newline at end of file diff --git a/packages/react/storybook/src/pages/Nested/Nested.stories.tsx b/packages/react/storybook/src/pages/Nested/Nested.stories.tsx index 66aa50c6..f037db9d 100644 --- a/packages/react/storybook/src/pages/Nested/Nested.stories.tsx +++ b/packages/react/storybook/src/pages/Nested/Nested.stories.tsx @@ -207,28 +207,30 @@ const MarkdownMark = ({children, value, style}: MarkdownMarkProps) => ( {children || value} ) +function TabbedMarkdownView({value, onChange}: {value: string; onChange?: (v: string) => void}) { + const {Tab, activeTab} = useTab([ + {value: 'preview', label: 'Preview'}, + {value: 'write', label: 'Write'}, + ]) + + return ( + <> + + + {activeTab === 'preview' ? ( + + ) : ( + + )} + + ) +} + export const ComplexMarkdown: Story = { args: { value: COMPLEX_MARKDOWN, }, - render: args => { - const {Tab, activeTab} = useTab([ - {value: 'preview', label: 'Preview'}, - {value: 'write', label: 'Write'}, - ]) - - return ( - <> - - - {activeTab === 'preview' ? ( - - ) : ( - - )} - - ) - }, + render: args => , } // ============================================================================ @@ -388,6 +390,31 @@ const HtmlDocMark = ({children, value, nested}: MarkProps) => { return {children || nested} } +function TabbedHtmlView({value, onChange}: {value: string; onChange?: (v: string) => void}) { + const {Tab, activeTab} = useTab([ + {value: 'preview', label: 'Preview'}, + {value: 'write', label: 'Write'}, + ]) + + return ( + <> + + + {activeTab === 'preview' ? ( + + ) : ( + + )} + + ) +} + export const ComplexHtmlDocument: Story = { args: { value: `
@@ -452,28 +479,5 @@ export const ComplexHtmlDocument: Story = {
`, }, - render: args => { - const {Tab, activeTab} = useTab([ - {value: 'preview', label: 'Preview'}, - {value: 'write', label: 'Write'}, - ]) - - return ( - <> - - - {activeTab === 'preview' ? ( - - ) : ( - - )} - - ) - }, + render: args => , } \ No newline at end of file diff --git a/packages/react/storybook/src/pages/Slots/Slots.stories.tsx b/packages/react/storybook/src/pages/Slots/Slots.stories.tsx index dec83bb5..0bbb6358 100644 --- a/packages/react/storybook/src/pages/Slots/Slots.stories.tsx +++ b/packages/react/storybook/src/pages/Slots/Slots.stories.tsx @@ -64,13 +64,52 @@ export const CustomComponents: StoryObj> = { span: FancySpan, }, }, - render: args => ( +} + +function EventLogStory(args: MarkedInputProps) { + const [events, setEvents] = useState([]) + + const addEvent = (event: string) => { + setEvents(prev => [...prev.slice(-4), event]) + } + + const slotProps = { + ...args.slotProps, + container: { + ...args.slotProps?.container, + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + addEvent('Enter pressed') + } + }, + onClick: () => addEvent('Clicked'), + onFocus: () => addEvent('Focused'), + onBlur: () => addEvent('Blurred'), + }, + } + + return ( <> -

Custom Components via Slots

-

Replace default div/span with your own components:

- +

Styling & Events via slotProps

+

Customize styling and add custom event handlers without replacing components:

+ + + +
+ Recent events: + {events.length === 0 ? ( +

No events yet

+ ) : ( +
    + {events.map((event, i) => ( +
  • {event}
  • + ))} +
+ )} +
- ), + ) } /** @@ -99,51 +138,7 @@ export const WithSlotProps: StoryObj> = { }, }, }, - render: args => { - const [events, setEvents] = useState([]) - - const addEvent = (event: string) => { - setEvents(prev => [...prev.slice(-4), event]) - } - - const slotProps = { - ...args.slotProps, - container: { - ...args.slotProps?.container, - onKeyDown: (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - addEvent('Enter pressed') - } - }, - onClick: () => addEvent('Clicked'), - onFocus: () => addEvent('Focused'), - onBlur: () => addEvent('Blurred'), - }, - } - - return ( - <> -

Styling & Events via slotProps

-

Customize styling and add custom event handlers without replacing components:

- - - -
- Recent events: - {events.length === 0 ? ( -

No events yet

- ) : ( -
    - {events.map((event, i) => ( -
  • {event}
  • - ))} -
- )} -
- - ) - }, + render: args => , } const StyledContainer = ({ref, ...props}: React.HTMLAttributes & {ref?: React.Ref}) => ( @@ -178,15 +173,6 @@ export const StyleMerging: StoryObj> = { }, }, }, - render: args => ( - <> -

Style Merging

-

- When using both slots and slotProps, styles merge intelligently. slotProps styles override slot styles: -

- - - ), } /** @@ -214,20 +200,4 @@ export const DataAttributes: StoryObj> = { }, }, }, - render: args => ( - <> -

Data Attributes in camelCase

-

slotProps supports camelCase data attributes (React converts them to kebab-case):

- - - -
-

- Try inspecting the element: You'll see data-test-id, data-module, data-user-id - attributes on the container. These are automatically converted from camelCase (dataTestId → - data-test-id). -

-
- - ), } \ No newline at end of file From d157b60a2b064f051546a6fb41e252812d7bd628 Mon Sep 17 00:00:00 2001 From: Nowely Date: Mon, 16 Mar 2026 00:09:55 +0300 Subject: [PATCH 10/11] refactor(storybook): enhance story components with plain value integration - Added `setupFiles` to Vite configuration for Vitest integration. - Introduced `vitest.setup.ts` for configuring Storybook preview annotations. - Enhanced `preview.ts` with decorators and global types for better story management. - Updated multiple story files to utilize the new `withPlainValue` decorator for improved plain value display and interaction. - Streamlined rendering logic in various stories by adopting args for props, enhancing clarity and maintainability. --- packages/vue/storybook/.storybook/preview.ts | 18 ++ .../storybook/src/pages/Base/Base.stories.ts | 59 ++--- .../storybook/src/pages/Drag/Drag.stories.ts | 84 ++----- .../src/pages/Dynamic/Dynamic.stories.ts | 59 +---- .../src/pages/Nested/Nested.stories.ts | 192 ++++++-------- .../src/pages/Overlay/Overlay.stories.ts | 88 ++----- .../src/pages/Slots/Slots.stories.ts | 235 +++++++----------- .../src/shared/lib/withPlainValue.ts | 78 ++++++ packages/vue/storybook/vite.config.ts | 1 + packages/vue/storybook/vitest.setup.ts | 5 + 10 files changed, 359 insertions(+), 460 deletions(-) create mode 100644 packages/vue/storybook/src/shared/lib/withPlainValue.ts create mode 100644 packages/vue/storybook/vitest.setup.ts diff --git a/packages/vue/storybook/.storybook/preview.ts b/packages/vue/storybook/.storybook/preview.ts index 6f818812..cb178b1f 100644 --- a/packages/vue/storybook/.storybook/preview.ts +++ b/packages/vue/storybook/.storybook/preview.ts @@ -1,6 +1,24 @@ import type {Preview} from '@storybook/vue3-vite' +import {withPlainValue} from '../src/shared/lib/withPlainValue' + const preview: Preview = { + decorators: [withPlainValue], + globalTypes: { + showPlainValue: { + name: 'Plain Value', + description: 'Plain value panel position', + defaultValue: 'right', + toolbar: { + icon: 'sidebaralt', + items: [ + {value: 'right', title: 'Show right', icon: 'sidebaralt'}, + {value: 'bottom', title: 'Show bottom', icon: 'bottombar'}, + {value: 'hide', title: 'Hide', icon: 'eyeclose'}, + ], + }, + }, + }, parameters: { controls: { hideNoControlsWarning: true, diff --git a/packages/vue/storybook/src/pages/Base/Base.stories.ts b/packages/vue/storybook/src/pages/Base/Base.stories.ts index 84ab5a5e..dcdeac83 100644 --- a/packages/vue/storybook/src/pages/Base/Base.stories.ts +++ b/packages/vue/storybook/src/pages/Base/Base.stories.ts @@ -1,10 +1,9 @@ -import type {MarkProps, MarkToken, Markup, Option} from '@markput/vue' -import {denote, MarkedInput} from '@markput/vue' +import type {MarkProps, Markup, Option} from '@markput/vue' +import {MarkedInput} from '@markput/vue' import type {Meta, StoryObj} from '@storybook/vue3-vite' -import {defineComponent, h, ref, computed} from 'vue' +import {defineComponent, h} from 'vue' import Button from '../../shared/components/Button.vue' -import Text from '../../shared/components/Text.vue' export default { title: 'MarkedInput', @@ -49,42 +48,24 @@ const configuredOptions = [ ] as Option[] export const Configured: Story = { - render: () => - defineComponent({ - setup() { - const value = ref( - "Enter the '@' for calling @[primary](primary:4) suggestions and '/' for @[default](default:7)!\n" + - 'Mark is can be a any component with any logic. In this example it is the @[Button](primary:54): clickable primary or secondary.\n' + - 'For found mark used @[annotations](default:123).' - ) - - const displayText = computed(() => - denote(value.value, (mark: MarkToken) => mark.value, [PrimaryMarkup, DefaultMarkup]) - ) - - return () => [ - h(MarkedInput, { - Mark: Button, - options: configuredOptions, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - slotProps: { - container: { - onClick: () => console.log('onClick'), - onInput: () => console.log('onInput'), - onBlur: () => console.log('onBlur'), - onFocus: () => console.log('onFocus'), - onKeydown: () => console.log('onKeyDown'), - }, - }, - }), - h(Text, {label: 'Plain text:', value: value.value}), - h(Text, {label: 'Display text (denoted):', value: displayText.value}), - ] + parameters: {plainValue: 'right'}, + args: { + Mark: Button, + options: configuredOptions, + value: + "Enter the '@' for calling @[primary](primary:4) suggestions and '/' for @[default](default:7)!\n" + + 'Mark is can be a any component with any logic. In this example it is the @[Button](primary:54): clickable primary or secondary.\n' + + 'For found mark used @[annotations](default:123).', + slotProps: { + container: { + onClick: () => console.log('onClick'), + onInput: () => console.log('onInput'), + onBlur: () => console.log('onBlur'), + onFocus: () => console.log('onFocus'), + onKeydown: () => console.log('onKeyDown'), }, - }), + }, + }, } export const Autocomplete: Story = { diff --git a/packages/vue/storybook/src/pages/Drag/Drag.stories.ts b/packages/vue/storybook/src/pages/Drag/Drag.stories.ts index 5477601b..4eb792b7 100644 --- a/packages/vue/storybook/src/pages/Drag/Drag.stories.ts +++ b/packages/vue/storybook/src/pages/Drag/Drag.stories.ts @@ -1,9 +1,7 @@ import type {MarkProps, Markup, Option} from '@markput/vue' import {MarkedInput} from '@markput/vue' import type {Meta, StoryObj} from '@storybook/vue3-vite' -import {defineComponent, h, markRaw, ref} from 'vue' - -import Text from '../../shared/components/Text.vue' +import {defineComponent, h, ref} from 'vue' export default { title: 'MarkedInput/Drag', @@ -131,26 +129,14 @@ const result = parser.parse('Hello **world**!') Visit our docs for more details.` export const Markdown: Story = { - render: () => - defineComponent({ - setup() { - const value = ref(DRAG_MARKDOWN) - return () => - h('div', {style: mdContainerStyle}, [ - h(MarkedInput, { - Mark: MarkdownMark, - options: markdownOptions, - value: value.value, - drag: true, - style: mdEditorStyle, - onChange: (v: string) => { - value.value = v - }, - }), - h(Text, {label: 'Raw value:', value: value.value}), - ]) - }, - }), + parameters: {plainValue: 'right'}, + args: { + Mark: MarkdownMark, + options: markdownOptions, + value: DRAG_MARKDOWN, + drag: true, + style: {...mdEditorStyle, ...mdContainerStyle}, + }, } // ─── Todo list (all marks include \n\n) ────────────────────────────────────── @@ -267,7 +253,7 @@ export const PlainTextDrag: Story = { value.value = v }, }), - h(Text, {value: value.value}), + h('pre', {}, value.value), ]) }, }), @@ -284,7 +270,7 @@ export const MarkdownDrag: Story = { return () => h('div', {}, [ h(MarkedInput, { - Mark: markRaw(MarkdownMark), + Mark: MarkdownMark, options: markdownOptions, value: value.value, drag: true, @@ -293,7 +279,7 @@ export const MarkdownDrag: Story = { value.value = v }, }), - h(Text, {value: value.value}), + h('pre', {}, value.value), ]) }, }), @@ -301,39 +287,21 @@ export const MarkdownDrag: Story = { export const ReadOnlyDrag: Story = { parameters: {docs: {disable: true}}, - render: () => - defineComponent({ - setup() { - return () => - h(MarkedInput, { - value: 'Read-Only Content\n\nSection A\n\nSection B', - readOnly: true, - drag: true, - style: testStyle, - }) - }, - }), + args: { + value: 'Read-Only Content\n\nSection A\n\nSection B', + readOnly: true, + drag: true, + style: testStyle, + }, } export const TodoListDrag: Story = { - render: () => - defineComponent({ - setup() { - const value = ref(TODO_VALUE) - return () => - h('div', {style: mdContainerStyle}, [ - h(MarkedInput, { - Mark: TodoMark, - options: todoOptions, - value: value.value, - drag: true, - style: {...mdEditorStyle, minHeight: '300px'}, - onChange: (v: string) => { - value.value = v - }, - }), - h(Text, {label: 'Raw value:', value: value.value}), - ]) - }, - }), + parameters: {plainValue: 'right'}, + args: { + Mark: TodoMark, + options: todoOptions, + value: TODO_VALUE, + drag: true, + style: {...mdEditorStyle, ...mdContainerStyle, minHeight: '300px'}, + }, } \ No newline at end of file diff --git a/packages/vue/storybook/src/pages/Dynamic/Dynamic.stories.ts b/packages/vue/storybook/src/pages/Dynamic/Dynamic.stories.ts index 42d4c2b4..d6d821ce 100644 --- a/packages/vue/storybook/src/pages/Dynamic/Dynamic.stories.ts +++ b/packages/vue/storybook/src/pages/Dynamic/Dynamic.stories.ts @@ -2,8 +2,6 @@ import {MarkedInput, useMark} from '@markput/vue' import type {Meta, StoryObj} from '@storybook/vue3-vite' import {defineComponent, h, ref, onMounted, watch, type ComponentPublicInstance} from 'vue' -import Text from '../../shared/components/Text.vue' - export default { title: 'MarkedInput/Mark', tags: ['autodocs'], @@ -40,20 +38,10 @@ const DynamicMark = defineComponent({ }) export const Dynamic: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('Hello, dynamical mark @[world]( )!') - return () => - h(MarkedInput, { - Mark: DynamicMark, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - }) - }, - }), + args: { + Mark: DynamicMark, + defaultValue: 'Hello, dynamical mark @[world]( )!', + }, } const RemovableMark = defineComponent({ @@ -64,20 +52,10 @@ const RemovableMark = defineComponent({ }) export const Removable: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('I @[contain]( ) @[removable]( ) by click @[marks]( )!') - return () => - h(MarkedInput, { - Mark: RemovableMark, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - }) - }, - }), + args: { + Mark: RemovableMark, + defaultValue: 'I @[contain]( ) @[removable]( ) by click @[marks]( )!', + }, } const Abbr = defineComponent({ @@ -113,20 +91,9 @@ const Abbr = defineComponent({ }) export const Focusable: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('Hello, @[focusable](By key operations) abbreviation @[world](Hello! Hello!)!') - return () => [ - h(MarkedInput, { - Mark: Abbr, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - }), - h(Text, {label: 'Plain text:', value: value.value}), - ] - }, - }), + parameters: {plainValue: 'right'}, + args: { + Mark: Abbr, + value: 'Hello, @[focusable](By key operations) abbreviation @[world](Hello! Hello!)!', + }, } \ No newline at end of file diff --git a/packages/vue/storybook/src/pages/Nested/Nested.stories.ts b/packages/vue/storybook/src/pages/Nested/Nested.stories.ts index 12e8edb0..bffa0988 100644 --- a/packages/vue/storybook/src/pages/Nested/Nested.stories.ts +++ b/packages/vue/storybook/src/pages/Nested/Nested.stories.ts @@ -1,9 +1,7 @@ import type {Markup} from '@markput/vue' import {MarkedInput} from '@markput/vue' import type {Meta, StoryObj} from '@storybook/vue3-vite' -import {defineComponent, h, ref} from 'vue' - -import Text from '../../shared/components/Text.vue' +import {defineComponent, h} from 'vue' export default { title: 'MarkedInput/Nested', @@ -35,40 +33,30 @@ const SimpleMark = defineComponent({ }, }) -export const SimpleNesting: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('This is *italic text with **bold** inside* and more text.') - - return () => [ - h(MarkedInput, { - Mark: SimpleMark, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - options: [ - { - markup: BoldMarkup, - mark: ({children}: {value?: string; children?: any}) => ({ - children, - style: {fontWeight: 'bold'}, - }), - }, - { - markup: ItalicMarkup, - mark: ({children}: {value?: string; children?: any}) => ({ - children, - style: {fontStyle: 'italic'}, - }), - }, - ], - }), - h(Text, {label: 'Raw value:', value: value.value}), - ] - }, +const simpleNestingOptions = [ + { + markup: BoldMarkup, + mark: ({children}: {value?: string; children?: any}) => ({ + children, + style: {fontWeight: 'bold'}, }), + }, + { + markup: ItalicMarkup, + mark: ({children}: {value?: string; children?: any}) => ({ + children, + style: {fontStyle: 'italic'}, + }), + }, +] + +export const SimpleNesting: Story = { + parameters: {plainValue: 'right'}, + args: { + Mark: SimpleMark, + value: 'This is *italic text with **bold** inside* and more text.', + options: simpleNestingOptions, + }, } // ============================================================================ @@ -86,67 +74,55 @@ const MultiLevelMark = defineComponent({ }, }) -export const MultipleLevels: Story = { - render: () => - defineComponent({ - setup() { - const value = ref( - 'Check #[this tag with @[nested mention with `code`]] and #[another #[deeply nested] tag]' - ) - - return () => [ - h(MarkedInput, { - Mark: MultiLevelMark, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - options: [ - { - markup: TagMarkup, - mark: ({children}: {value?: string; children?: any}) => ({ - children, - style: { - backgroundColor: '#e7f3ff', - border: '1px solid #2196f3', - color: '#1976d2', - padding: '2px 6px', - borderRadius: '4px', - }, - }), - }, - { - markup: MentionMarkup, - mark: ({children}: {value?: string; children?: any}) => ({ - children, - style: { - backgroundColor: '#fff3e0', - border: '1px solid #ff9800', - color: '#f57c00', - padding: '2px 6px', - borderRadius: '4px', - }, - }), - }, - { - markup: CodeMarkup, - mark: ({children}: {value?: string; children?: any}) => ({ - children, - style: { - backgroundColor: '#f3e5f5', - border: '1px solid #9c27b0', - color: '#7b1fa2', - padding: '2px 6px', - borderRadius: '4px', - }, - }), - }, - ], - }), - h(Text, {label: 'Raw value:', value: value.value}), - ] +const multipleLevelsOptions = [ + { + markup: TagMarkup, + mark: ({children}: {value?: string; children?: any}) => ({ + children, + style: { + backgroundColor: '#e7f3ff', + border: '1px solid #2196f3', + color: '#1976d2', + padding: '2px 6px', + borderRadius: '4px', }, }), + }, + { + markup: MentionMarkup, + mark: ({children}: {value?: string; children?: any}) => ({ + children, + style: { + backgroundColor: '#fff3e0', + border: '1px solid #ff9800', + color: '#f57c00', + padding: '2px 6px', + borderRadius: '4px', + }, + }), + }, + { + markup: CodeMarkup, + mark: ({children}: {value?: string; children?: any}) => ({ + children, + style: { + backgroundColor: '#f3e5f5', + border: '1px solid #9c27b0', + color: '#7b1fa2', + padding: '2px 6px', + borderRadius: '4px', + }, + }), + }, +] + +export const MultipleLevels: Story = { + parameters: {plainValue: 'right'}, + args: { + Mark: MultiLevelMark, + value: 'Check #[this tag with @[nested mention with `code`]] and #[another #[deeply nested] tag]', + options: multipleLevelsOptions, + }, } // ============================================================================ @@ -163,24 +139,10 @@ const HtmlLikeMark = defineComponent({ }) export const HtmlLikeTags: Story = { - render: () => - defineComponent({ - setup() { - const value = ref( - '
This is a div with a mark inside and bold text with nested del
' - ) - - return () => [ - h(MarkedInput, { - Mark: HtmlLikeMark, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - options: [{markup: HtmlMarkup}], - }), - h(Text, {label: 'Raw value:', value: value.value}), - ] - }, - }), + parameters: {plainValue: 'right'}, + args: { + Mark: HtmlLikeMark, + value: '
This is a div with a mark inside and bold text with nested del
', + options: [{markup: HtmlMarkup}], + }, } \ No newline at end of file diff --git a/packages/vue/storybook/src/pages/Overlay/Overlay.stories.ts b/packages/vue/storybook/src/pages/Overlay/Overlay.stories.ts index 684ab24e..9efd40ee 100644 --- a/packages/vue/storybook/src/pages/Overlay/Overlay.stories.ts +++ b/packages/vue/storybook/src/pages/Overlay/Overlay.stories.ts @@ -1,6 +1,6 @@ import {MarkedInput, useOverlay} from '@markput/vue' import type {Meta, StoryObj} from '@storybook/vue3-vite' -import {defineComponent, h, ref, type ComponentPublicInstance} from 'vue' +import {defineComponent, h, type ComponentPublicInstance} from 'vue' export default { title: 'MarkedInput/Overlay', @@ -38,41 +38,23 @@ const CustomOverlayComponent = defineComponent({ }, }) +const EmptyMark = defineComponent({setup: () => () => null}) + export const CustomOverlay: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('Hello, custom overlay by trigger @!') - return () => - h(MarkedInput, { - Mark: defineComponent({setup: () => () => null}), - Overlay: CustomOverlayComponent, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - }) - }, - }), + args: { + Mark: EmptyMark, + Overlay: CustomOverlayComponent, + defaultValue: 'Hello, custom overlay by trigger @!', + }, } export const CustomTrigger: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('Hello, custom overlay by trigger /!') - return () => - h(MarkedInput, { - Mark: defineComponent({setup: () => () => null}), - Overlay: CustomOverlayComponent, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - options: [{overlay: {trigger: '/'}}], - }) - }, - }), + args: { + Mark: EmptyMark, + Overlay: CustomOverlayComponent, + defaultValue: 'Hello, custom overlay by trigger /!', + options: [{overlay: {trigger: '/'}}], + }, } const Tooltip = defineComponent({ @@ -94,21 +76,11 @@ const Tooltip = defineComponent({ }) export const PositionedOverlay: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('Hello, positioned overlay by trigger @!') - return () => - h(MarkedInput, { - Mark: defineComponent({setup: () => () => null}), - Overlay: Tooltip, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - }) - }, - }), + args: { + Mark: EmptyMark, + Overlay: Tooltip, + defaultValue: 'Hello, positioned overlay by trigger @!', + }, } const List = defineComponent({ @@ -131,20 +103,10 @@ const List = defineComponent({ }) export const SelectableOverlay: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('Hello, suggest overlay by trigger @!') - return () => - h(MarkedInput, { - Mark, - Overlay: List, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - options: [{markup: '@[__value__](__meta__)' as any, overlay: {trigger: '@'}}], - }) - }, - }), + args: { + Mark, + Overlay: List, + defaultValue: 'Hello, suggest overlay by trigger @!', + options: [{markup: '@[__value__](__meta__)' as any, overlay: {trigger: '@'}}], + }, } \ No newline at end of file diff --git a/packages/vue/storybook/src/pages/Slots/Slots.stories.ts b/packages/vue/storybook/src/pages/Slots/Slots.stories.ts index e9b88d89..5e7e0094 100644 --- a/packages/vue/storybook/src/pages/Slots/Slots.stories.ts +++ b/packages/vue/storybook/src/pages/Slots/Slots.stories.ts @@ -2,8 +2,6 @@ import {MarkedInput} from '@markput/vue' import type {Meta, StoryObj} from '@storybook/vue3-vite' import {defineComponent, h, ref, reactive} from 'vue' -import Text from '../../shared/components/Text.vue' - const meta = { title: 'API/Slots', component: MarkedInput, @@ -33,153 +31,112 @@ const SimpleMark = defineComponent({ }, }) -export const WithSlotProps: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('Try pressing @[Enter] or clicking') - const events = reactive([]) +const EventLogComponent = defineComponent({ + props: {Mark: {type: null}, value: String, className: String}, + setup(props) { + const currentValue = ref(props.value ?? '') + const events = reactive([]) - const addEvent = (event: string) => { - if (events.length > 4) events.splice(0, events.length - 4) - events.push(event) - } + const addEvent = (event: string) => { + if (events.length > 4) events.splice(0, events.length - 4) + events.push(event) + } - return () => [ - h('h3', 'Styling & Events via slotProps'), - h('p', 'Customize styling and add custom event handlers without replacing components:'), - h(MarkedInput, { - Mark: SimpleMark, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - className: 'custom-container', - slotProps: { - container: { - onKeydown: (e: KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - addEvent('Enter pressed') - } - }, - onClick: () => addEvent('Clicked'), - onFocus: () => addEvent('Focused'), - onBlur: () => addEvent('Blurred'), - style: { - border: '2px solid #4CAF50', - borderRadius: '8px', - padding: '12px', - backgroundColor: '#f5f5f5', - }, - }, - span: { - style: { - color: '#333', - fontSize: '14px', - }, - }, - }, - }), - h( - 'div', - {style: {marginTop: '16px', padding: '12px', backgroundColor: '#f0f0f0', borderRadius: '4px'}}, - [ - h('strong', 'Recent events:'), - events.length === 0 - ? h('p', {style: {marginTop: '8px', color: '#666'}}, 'No events yet') - : h( - 'ul', - {style: {marginTop: '8px', paddingLeft: '20px'}}, - events.map((event, i) => h('li', {key: i}, event)) - ), - ] - ), - h(Text, {label: 'Raw value:', value: value.value}), - ] - }, - }), -} - -export const StyleMerging: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('Container has @[merged] styles from multiple sources') - - return () => [ - h('h3', 'Style Merging'), - h( - 'p', - 'When using both className/style and slotProps, styles merge intelligently. slotProps styles override component styles:' - ), - h(MarkedInput, { - Mark: SimpleMark, - value: value.value, - onChange: (v: string) => { - value.value = v + return () => [ + h('h3', 'Styling & Events via slotProps'), + h('p', 'Customize styling and add custom event handlers without replacing components:'), + h(MarkedInput, { + Mark: (props.Mark as any) ?? SimpleMark, + value: currentValue.value, + onChange: (v: string) => { + currentValue.value = v + }, + className: props.className, + slotProps: { + container: { + onKeydown: (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + addEvent('Enter pressed') + } }, + onClick: () => addEvent('Clicked'), + onFocus: () => addEvent('Focused'), + onBlur: () => addEvent('Blurred'), style: { - background: '#e3f2fd', + border: '2px solid #4CAF50', borderRadius: '8px', + padding: '12px', + backgroundColor: '#f5f5f5', }, - slotProps: { - container: { - style: { - padding: '16px', - border: '2px solid #1976d2', - }, - }, + }, + span: { + style: { + color: '#333', + fontSize: '14px', }, - }), - h(Text, {label: 'Raw value:', value: value.value}), - ] + }, + }, + }), + h('div', {style: {marginTop: '16px', padding: '12px', backgroundColor: '#f0f0f0', borderRadius: '4px'}}, [ + h('strong', 'Recent events:'), + events.length === 0 + ? h('p', {style: {marginTop: '8px', color: '#666'}}, 'No events yet') + : h( + 'ul', + {style: {marginTop: '8px', paddingLeft: '20px'}}, + events.map((event, i) => h('li', {key: i}, event)) + ), + ]), + ] + }, +}) + +export const WithSlotProps: Story = { + args: {Mark: SimpleMark, value: 'Try pressing @[Enter] or clicking', className: 'custom-container'}, + render: () => EventLogComponent, +} + +export const StyleMerging: Story = { + parameters: {plainValue: 'right'}, + args: { + Mark: SimpleMark, + value: 'Container has @[merged] styles from multiple sources', + style: { + background: '#e3f2fd', + borderRadius: '8px', + }, + slotProps: { + container: { + style: { + padding: '16px', + border: '2px solid #1976d2', + }, }, - }), + }, + }, } export const DataAttributes: Story = { - render: () => - defineComponent({ - setup() { - const value = ref('Use @[data] attributes for testing and tracking') - - return () => [ - h('h3', 'Data Attributes in camelCase'), - h('p', 'slotProps supports camelCase data attributes (converted to kebab-case):'), - h(MarkedInput, { - Mark: SimpleMark, - value: value.value, - onChange: (v: string) => { - value.value = v - }, - slotProps: { - container: { - dataTestId: 'marked-input-demo', - dataModule: 'slots-api', - dataUserId: 'user-123', - style: { - border: '1px solid #999', - padding: '12px', - borderRadius: '4px', - backgroundColor: '#f9f9f9', - }, - }, - span: { - dataTokenType: 'text', - }, - }, - }), - h( - 'div', - {style: {marginTop: '16px', padding: '12px', backgroundColor: '#f0f0f0', borderRadius: '4px'}}, - h('p', {style: {marginTop: 0}}, [ - h('strong', 'Try inspecting the element:'), - " You'll see data-test-id, data-module, data-user-id attributes on the container.", - ]) - ), - h(Text, {label: 'Raw value:', value: value.value}), - ] + parameters: {plainValue: 'right'}, + args: { + Mark: SimpleMark, + value: 'Use @[data] attributes for testing and tracking', + slotProps: { + container: { + dataTestId: 'marked-input-demo', + dataModule: 'slots-api', + dataUserId: 'user-123', + style: { + border: '1px solid #999', + padding: '12px', + borderRadius: '4px', + backgroundColor: '#f9f9f9', + }, }, - }), + span: { + dataTokenType: 'text', + }, + }, + }, } \ No newline at end of file diff --git a/packages/vue/storybook/src/shared/lib/withPlainValue.ts b/packages/vue/storybook/src/shared/lib/withPlainValue.ts new file mode 100644 index 00000000..3e6d080d --- /dev/null +++ b/packages/vue/storybook/src/shared/lib/withPlainValue.ts @@ -0,0 +1,78 @@ +import {useArgs, useGlobals} from 'storybook/preview-api' +import {defineComponent, h, ref} from 'vue' + +export const withPlainValue = (story: any, context: any) => { + // Storybook hooks — ok to call here (hookify wrapper active at decorator level) + const [args, updateArgs] = useArgs() + const [globals] = useGlobals() + + const mergedArgs = {...context.args, ...args} + const isControlled = 'value' in mergedArgs + const rawPosition = context.parameters?.plainValue as 'right' | 'bottom' | undefined + const showPanel = rawPosition === 'right' || rawPosition === 'bottom' + const globalValue = (globals.showPlainValue ?? 'right') as 'right' | 'bottom' | 'hide' + const showPlainValue = globalValue !== 'hide' + const globalPosition: 'right' | 'bottom' = globalValue === 'hide' ? 'right' : globalValue + + // Stories that don't opt in to the panel, or are uncontrolled. + if (!showPanel || !isControlled) { + return story() + } + + // Panel opted in but globally hidden — still wire onChange so controls stay in sync. + if (!showPlainValue) { + return defineComponent({ + setup: () => () => h(story(), {onChange: (v: string) => updateArgs({value: v})}), + }) + } + + const position = rawPosition ?? globalPosition + + return defineComponent({ + setup() { + const value = ref(mergedArgs.value ?? '') + + return () => { + const storyNode = h(story(), { + value: value.value, + onChange: (v: string) => { + value.value = v + }, + }) + const preNode = h( + 'pre', + {style: {padding: '8px', fontFamily: 'monospace', fontSize: '14px', margin: 0}}, + value.value || '' + ) + + if (position === 'right') { + return h('div', {style: {display: 'flex', gap: '16px', height: '100%'}}, [ + h('div', {style: {flex: 3, minWidth: 0}}, [storyNode]), + h('div', {style: {flex: 1, minWidth: 0}}, [ + h( + 'div', + { + style: { + fontSize: '11px', + fontWeight: 'bold', + color: '#666', + marginBottom: '4px', + textTransform: 'uppercase', + }, + }, + 'Plain Value' + ), + preNode, + ]), + ]) + } + + return h('div', {}, [ + storyNode, + h('hr', {style: {margin: '8px 0', border: '1px solid #e0e0e0'}}), + preNode, + ]) + } + }, + }) +} \ No newline at end of file diff --git a/packages/vue/storybook/vite.config.ts b/packages/vue/storybook/vite.config.ts index 63fecb69..d6d9db03 100644 --- a/packages/vue/storybook/vite.config.ts +++ b/packages/vue/storybook/vite.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ }, test: { globals: true, + setupFiles: ['./vitest.setup.ts'], include: ['src/pages/**/*.spec.ts'], coverage: { provider: 'v8', diff --git a/packages/vue/storybook/vitest.setup.ts b/packages/vue/storybook/vitest.setup.ts new file mode 100644 index 00000000..0eb832da --- /dev/null +++ b/packages/vue/storybook/vitest.setup.ts @@ -0,0 +1,5 @@ +import {setProjectAnnotations} from '@storybook/vue3-vite' + +import preview from './.storybook/preview' + +setProjectAnnotations(preview) \ No newline at end of file From ce9dbe94cde15c1cabf1ca6d286c7598db51d5fa Mon Sep 17 00:00:00 2001 From: Nowely Date: Mon, 16 Mar 2026 00:11:21 +0300 Subject: [PATCH 11/11] refactor(storybook): update plainValue parameter in Configured story - Changed the plainValue parameter from 'right' to 'bottom' in the Configured story for improved layout consistency. - Ensured alignment with recent updates to story components and their configurations. --- packages/react/storybook/src/pages/Base/Base.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/storybook/src/pages/Base/Base.stories.tsx b/packages/react/storybook/src/pages/Base/Base.stories.tsx index 9eca2b8e..ccaa98ad 100644 --- a/packages/react/storybook/src/pages/Base/Base.stories.tsx +++ b/packages/react/storybook/src/pages/Base/Base.stories.tsx @@ -44,7 +44,7 @@ const configuredOptions: Option[] = [ ] export const Configured: StoryObj> = { - parameters: {plainValue: 'right'}, + parameters: {plainValue: 'bottom'}, args: { Mark: Button, options: configuredOptions,