diff --git a/packages/react/storybook/.storybook/preview.ts b/packages/react/storybook/.storybook/preview.ts index 1dbcd053..ddb66c78 100644 --- a/packages/react/storybook/.storybook/preview.ts +++ b/packages/react/storybook/.storybook/preview.ts @@ -1,16 +1,20 @@ import type {Preview} from '@storybook/react-vite' +import {withPlainValue} from '../src/shared/lib/withPlainValue' + const preview: Preview = { + decorators: [withPlainValue], 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/pages/Ant/Ant.stories.tsx b/packages/react/storybook/src/pages/Ant/Ant.stories.tsx index 8c2711a2..1b190dfa 100644 --- a/packages/react/storybook/src/pages/Ant/Ant.stories.tsx +++ b/packages/react/storybook/src/pages/Ant/Ant.stories.tsx @@ -1,34 +1,24 @@ +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 {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.` - ) - - return ( - <> - ({children: value, color: value, style: {marginRight: 0}}), - }, - ]} - /> +} satisfies Meta - - - ) +export const Tagged: StoryObj> = { + 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/Base/Base.stories.tsx b/packages/react/storybook/src/pages/Base/Base.stories.tsx index b86d7d06..ccaa98ad 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, 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 {Text} from '../../shared/components/Text' +import type {ButtonProps} from '../../shared/components/Button' export default { title: 'MarkedInput', @@ -25,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)}), @@ -44,38 +43,24 @@ const configuredOptions = [ }, ] -export const Configured: Story = { - render: () => { - const [value, setValue] = useState( +export const Configured: StoryObj> = { + parameters: {plainValue: 'bottom'}, + 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).' - ) - - 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'), - }, - }} - /> - - - - - ) + '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/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 96b43270..a65da1dd 100644
--- a/packages/react/storybook/src/pages/Drag/Drag.stories.tsx
+++ b/packages/react/storybook/src/pages/Drag/Drag.stories.tsx
@@ -1,12 +1,10 @@
 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 +25,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)
-
-		return (
-			
- - -
- ) +interface MarkdownMarkProps extends MarkProps { + style?: CSSProperties +} + +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,55 +179,45 @@ 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}}, - 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' - ) - return ( - <> - - - - ) + 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, }, } -export const MarkdownDrag: Story = { - parameters: {docs: {disable: true}}, - 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' - ) - return ( - <> - - - - ) +export const MarkdownDrag: StoryObj> = { + parameters: {docs: {disable: true}, plainValue: 'bottom'}, + 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, }, } export const ReadOnlyDrag: Story = { parameters: {docs: {disable: true}}, - render: () => , + args: { + value: 'Read-Only Content\n\nSection A\n\nSection B', + readOnly: true, + drag: true, + style: testStyle, + }, } // ─── Todo list (all marks include \n\n) ─────────────────────────────────────── export const TodoList: StoryObj> = { - decorators: [withPlainValue], args: { Mark: TodoMark, options: todoOptions, value: TODO_VALUE, drag: true, }, + parameters: { + plainValue: 'right', + }, } \ 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 8c799525..2f27c4b1 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}}, + args: { + Mark: RemovableMark, + value: 'I @[contain]( ) @[removable]( ) by click @[marks]( )!', + }, } const Abbr = () => { @@ -55,16 +62,18 @@ 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}, plainValue: 'right'}, + args: { + Mark: Abbr, + value: 'Hello, @[focusable](By key operations) abbreviation @[world](Hello! Hello!)!', + }, + decorators: [ + Story => { + useCaretInfo(true) + return + }, + ], } /*TODO @@ -92,7 +101,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/Experimental/Experimental.stories.tsx b/packages/react/storybook/src/pages/Experimental/Experimental.stories.tsx index cf99bff1..5d21ce77 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,7 @@ 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: () => {}} />, } /** @@ -59,21 +43,7 @@ export const Controlled: Story = { * This is the recommended approach for single contentEditable! */ export const Uncontrolled: Story = { - render: () => { - const [value, setValue] = useState('') - - return ( - <> -
-

✅ Uncontrolled (MutationObserver)

-
- - - - - - ) - }, + render: () => {}} />, } /** @@ -90,19 +60,5 @@ export const Uncontrolled: Story = { * Uses the same uncontrolled approach with MutationObserver for smooth editing! */ export const Markdown: Story = { - render: () => { - const [value, setValue] = useState('') - - return ( - <> -
-

📝 Markdown (Uncontrolled)

-
- - - - - - ) - }, + render: () => {}} />, } \ No newline at end of file diff --git a/packages/react/storybook/src/pages/Material/Material.stories.tsx b/packages/react/storybook/src/pages/Material/Material.stories.tsx index dfaf2694..bb1dc025 100644 --- a/packages/react/storybook/src/pages/Material/Material.stories.tsx +++ b/packages/react/storybook/src/pages/Material/Material.stories.tsx @@ -1,86 +1,68 @@ -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' import {useState} from 'react' -import {Text} from '../../shared/components/Text' import {MaterialMentions} from './components/MaterialMentions' export default { title: 'Styled/Material', component: MarkedInput, -} +} satisfies Meta 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) ' -export const Chipped = () => { +export const Chipped: StoryObj> = { + 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 = () => { const [value, setValue] = useState(initialValue) return ( - <> - ({label: value, variant: 'outlined' as const, size: 'small' as const}), + mark: ({value}: MarkProps) => ({ + label: value, + variant: 'outlined' as const, + size: 'small' as const, + }), }, { markup: '@[__value__](common:__meta__)', - mark: ({value}) => ({label: value, size: 'small' as const}), + mark: ({value}: MarkProps) => ({label: value, size: 'small' as const}), }, - ]} - /> - - - - ) -} - -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} - /> - -
- - + ], + }} + value={value} + onChange={setValue as any} + /> ) } \ 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 aab847d5..f037db9d 100644 --- a/packages/react/storybook/src/pages/Nested/Nested.stories.tsx +++ b/packages/react/storybook/src/pages/Nested/Nested.stories.tsx @@ -1,15 +1,14 @@ -import type {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 {ReactNode} from 'react' +import type {CSSProperties} 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' -export default { +const meta = { title: 'MarkedInput/Nested', tags: ['autodocs'], component: MarkedInput, @@ -23,7 +22,8 @@ export default { }, } satisfies Meta -type Story = StoryObj> +export default meta +type Story = StoryObj // ============================================================================ // Example 1: Simple Nesting (Markdown-style) @@ -32,42 +32,34 @@ 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} -) - -export const SimpleNesting: Story = { - render: () => { - const [value, setValue] = useState('This is *italic text with **bold** inside* and more text.') +interface SimpleMarkProps extends MarkProps { + style?: CSSProperties +} - return ( - <> - ({ - value, - children, - style: {fontWeight: 'bold'}, - }), - }, - { - markup: ItalicMarkup, - mark: ({value, children}) => ({ - value, - children, - style: {fontStyle: 'italic'}, - }), - }, - ]} - /> - - - ) +const SimpleMark = ({children, style, value}: SimpleMarkProps) => {children || value} + +export const SimpleNesting: StoryObj> = { + args: { + Mark: SimpleMark, + 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'}, + }), + }, + ], }, } @@ -79,76 +71,62 @@ 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 = { - 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', - }, - }), - }, - ]} - /> - - - ) +interface MultiLevelMarkProps extends MarkProps { + style?: CSSProperties +} + +const MultiLevelMark = ({children, style, value}: MultiLevelMarkProps) => ( + {children || value} +) + +export const MultipleLevels: StoryObj> = { + args: { + Mark: MultiLevelMark, + 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', + }, + }), + }, + ], }, } @@ -158,23 +136,16 @@ 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 = { - render: () => { - const [value, setValue] = useState( - '
This is a div with a mark inside and bold text with nested del
' - ) - - return ( - <> - - - - ) +export const HtmlLikeTags: StoryObj> = { + args: { + Mark: HtmlLikeMark, + value: '
This is a div with a mark inside and bold text with nested del
', + options: [{markup: HtmlMarkup}], }, } @@ -182,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) @@ -216,117 +187,57 @@ 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 ( - <> - - - - ) +export const InteractiveNested: StoryObj> = { + args: { + Mark: InteractiveMark, + value: '@[Click me @[or me @[or even me]]]', + options: [{markup: '@[__nested__]'}], }, } // ============================================================================ -// Example 5: Editable Nested Content +// Example 6: Complex Markdown Document // ============================================================================ -// 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. -//

-//
-// -// ) -// }, -// } +interface MarkdownMarkProps extends MarkProps { + style?: CSSProperties +} -// ============================================================================ -// Example 6: Complex Markdown Document -// ============================================================================ +const MarkdownMark = ({children, value, style}: MarkdownMarkProps) => ( + {children || value} +) -const MarkdownMark = ({ - children, - value, - style, -}: { - value?: string - children?: ReactNode - style?: React.CSSProperties -}) => {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 = { - render: () => { - const [value, setValue] = useState(COMPLEX_MARKDOWN) - const {Tab, activeTab} = useTab([ - {value: 'preview', label: 'Preview'}, - {value: 'write', label: 'Write'}, - ]) - - return ( - <> - - - {activeTab === 'preview' ? ( - - ) : ( - - )} - - ) + args: { + value: COMPLEX_MARKDOWN, }, + render: args => , } // ============================================================================ // 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 = { @@ -479,9 +390,34 @@ const HtmlDocMark = ({children, value, nested}: {value?: string; children?: Reac 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 = { - render: () => { - const [value, setValue] = useState(`
+ args: { + value: `

Understanding Nested HTML Structures

Published on

@@ -541,32 +477,7 @@ export const ComplexHtmlDocument: Story = {

© 2025 MarkedInput Library. Built with React and TypeScript.

-
`) - const {Tab, activeTab} = useTab([ - {value: 'preview', label: 'Preview'}, - {value: 'write', label: 'Write'}, - ]) - - return ( - <> - - - {activeTab === 'preview' ? ( - - ) : ( - - )} - - ) +
`, }, -} - -// ============================================================================ -// Example 8: Complex Real-World Example (Rich Text Editor) -// ============================================================================ \ No newline at end of file + render: args => , +} \ 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..2670dfc0 100644 --- a/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx +++ b/packages/react/storybook/src/pages/Rsuite/Rsuite.stories.tsx @@ -1,10 +1,11 @@ -import type {Markup} from '@markput/react' +import type {MarkProps, MarkedInputProps, 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' +import type {TagProps} from 'rsuite' -import {Text} from '../../shared/components/Text' import {withStyle} from '../../shared/lib/withStyle' export default { @@ -46,69 +47,54 @@ 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: '@'}, + }, + ]} + /> ) } -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: StoryObj> = { + 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..0bbb6358 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 type {MarkProps, MarkedInputProps} from '@markput/react' import {MarkedInput} from '@markput/react' import type {Meta, StoryObj} from '@storybook/react-vite' import {useState} from 'react' -import {Text} from '../../shared/components/Text' - const meta = { title: 'API/Slots', component: MarkedInput, @@ -19,195 +18,160 @@ const meta = { } satisfies Meta export default meta -type Story = StoryObj -const SimpleMark = ({children}: {value?: string; children?: React.ReactNode}) => ( +const SimpleMark = ({children}: MarkProps) => ( {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:

- - - - - - ) +export const CustomComponents: StoryObj> = { + args: { + Mark: SimpleMark, + value: 'Both @[container] and @[span] are @[customized]', + slots: { + container: FancyContainer, + span: FancySpan, + }, }, } +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 ( + <> +

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}
  • + ))} +
+ )} +
+ + ) +} + /** * 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 = { - render: () => { - const [value, setValue] = useState('Try pressing @[Enter] or clicking') - const [events, setEvents] = useState([]) - - const addEvent = (event: string) => { - setEvents(prev => [...prev.slice(-4), event]) - } - - 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: - {events.length === 0 ? ( -

No events yet

- ) : ( -
    - {events.map((event, i) => ( -
  • {event}
  • - ))} -
- )} -
- - - - ) +export const WithSlotProps: StoryObj> = { + args: { + Mark: SimpleMark, + 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 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: -

- - - - - - ) +export const StyleMerging: StoryObj> = { + args: { + Mark: SimpleMark, + value: 'Container has @[merged] styles from multiple sources', + slots: { + container: StyledContainer, + }, + slotProps: { + container: { + style: { + padding: '16px', + border: '2px solid #1976d2', + }, + }, + }, }, } @@ -215,47 +179,25 @@ 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 = { - 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). -

-
- - - - ) +export const DataAttributes: StoryObj> = { + 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/react/storybook/src/shared/components/Text/Text.css b/packages/react/storybook/src/shared/components/Text/Text.css index b110eae8..5155d018 100644 --- a/packages/react/storybook/src/shared/components/Text/Text.css +++ b/packages/react/storybook/src/shared/components/Text/Text.css @@ -1,79 +1,114 @@ -.text-container { - border: 1px solid #d0d7de; - border-radius: 6px; +/* ============================================================ + PlainValuePanel styles (pvp-*) + ============================================================ */ + +/* --- Dashed divider for bottom mode --- */ +.pvp-divider { + border: none; + 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..e9795c3a 100644 --- a/packages/react/storybook/src/shared/components/Text/Text.tsx +++ b/packages/react/storybook/src/shared/components/Text/Text.tsx @@ -1,18 +1,43 @@ +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 3b72165a..c11aa241 100644 --- a/packages/react/storybook/src/shared/lib/withPlainValue.tsx +++ b/packages/react/storybook/src/shared/lib/withPlainValue.tsx @@ -1,42 +1,113 @@ -import {useCallback} 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) => { - const [args, updateArgs] = useArgs() - const [globals] = useGlobals() - const showPlainValue = globals.showPlainValue !== 'hide' +// ─── 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. + +interface PanelContainerProps { + Story: any + args: any + value: string + position: 'right' | 'bottom' + updateArgs: (update: Record) => void +} + +function PanelContainer({Story, args, value: valueProp, position: positionProp, updateArgs}: PanelContainerProps) { + const [value, setValue] = useState(valueProp) + const [prevValueProp, setPrevValueProp] = useState(valueProp) + + // 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) + } + + const wrapperRef = useRef(null) + const [position, setPosition] = useState<'right' | 'bottom'>(positionProp) + + // Responsive: switch to 'bottom' when wrapper is narrower than 600px. + useEffect(() => { + const el = wrapperRef.current + if (!el) return + const observer = new ResizeObserver(([entry]) => { + setPosition(entry.contentRect.width < 600 ? 'bottom' : positionProp) + }) + observer.observe(el) + return () => observer.disconnect() + }, [positionProp]) const handleChange = useCallback( (newValue: string) => { + setValue(newValue) updateArgs({value: newValue}) }, [updateArgs] ) - if (!showPlainValue) { - return + const storyArgs = {...args, value, onChange: handleChange} + + if (position === 'right') { + return ( +
+
+ +
+ +
+ ) } return ( -
-
- -
- {args.value !== 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 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 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