diff --git a/src/components/i18n/i18n.stories.tsx b/src/components/i18n/i18n.stories.tsx new file mode 100644 index 00000000000..93f0426598f --- /dev/null +++ b/src/components/i18n/i18n.stories.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { hideStorybookControls } from '../../../.storybook/utils'; +import { EuiI18n, EuiI18nProps, I18nTokensShape } from './i18n'; +import { EuiCard } from '../card'; + +type Props = EuiI18nProps; + +const meta: Meta = { + title: 'Utilities/EuiI18n', + component: EuiI18n, +}; + +export default meta; +type Story = StoryObj; + +export const SingleToken: Story = { + argTypes: { + default: { control: { type: 'text' } }, + ...hideStorybookControls(['children', 'tokens', 'defaults']), + }, + args: { + token: 'euiI18nBasic.basicexample', + default: + 'This is the English copy that would be replaced by a translation defined by the euiI18nBasic.basicexample token.', + }, +}; + +export const Interpolation: Story = { + argTypes: { + ...hideStorybookControls(['children', 'tokens', 'defaults']), + }, + args: { + token: 'euiI18nInterpolation.clickedCount', + default: 'Clicked on button {count} times.', + values: { count: 3 }, + }, +}; + +export const MultipleTokens: Story = { + argTypes: { + ...hideStorybookControls(['token', 'default']), + }, + args: { + tokens: ['euiI18n.title', 'euiI18n.description'], + defaults: ['Card title', 'Card description'], + }, + render: ({ tokens, defaults }: I18nTokensShape) => ( + // eslint-disable-next-line local/i18n + + {([title, description]: string[]) => ( + + )} + + ), +}; + +export const MultipleTokenInterpolation: Story = { + argTypes: { + ...hideStorybookControls(['token', 'default']), + }, + args: { + tokens: ['euiI18nMulti.title', 'euiI18nMulti.description'], + defaults: [ + 'How often was the {name} cuddled?', + 'The {name} was cuddled {count} times.', + ], + values: { name: 'cat', count: 3 }, + }, + render: ({ tokens, defaults, values }: I18nTokensShape) => ( + // eslint-disable-next-line local/i18n + + {([title, description]: string[]) => ( + + )} + + ), +}; diff --git a/src/components/i18n/i18n.tsx b/src/components/i18n/i18n.tsx index 2659f36aa8d..4eef8881953 100644 --- a/src/components/i18n/i18n.tsx +++ b/src/components/i18n/i18n.tsx @@ -90,13 +90,19 @@ type ResolvedType = T extends (...args: any[]) => any ? ReturnType : T; interface I18nTokenShape> { token: string; default: DEFAULT; + /** + * Render function that returns a ReactElement + */ children?: (x: ResolvedType) => ReactChild; values?: T; } -interface I18nTokensShape { +export interface I18nTokensShape { tokens: string[]; defaults: T; + /** + * Render function that returns a ReactElement + */ children: (x: Array) => ReactChild; values?: Record; } diff --git a/src/components/i18n/i18n_number.stories.tsx b/src/components/i18n/i18n_number.stories.tsx new file mode 100644 index 00000000000..180c4067442 --- /dev/null +++ b/src/components/i18n/i18n_number.stories.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { ReactChild } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { hideStorybookControls } from '../../../.storybook/utils'; +import { EuiI18nNumber, EuiI18nNumberProps } from './i18n_number'; +import { EuiText } from '../text'; + +const meta: Meta = { + title: 'Utilities/EuiI18nNumber', + component: EuiI18nNumber, +}; + +export default meta; +type Story = StoryObj; + +export const SingleValue: Story = { + argTypes: hideStorybookControls(['children', 'values']), + args: { + value: 99, + }, + render: (args: EuiI18nNumberProps) => ( + + Formatted number: + + ), +}; + +export const MultipleValues: Story = { + argTypes: hideStorybookControls(['value']), + args: { + values: [0, 1, 2], + children: (values: ReactChild[]) => ( + <> + {values.map((value) => ( + + Formatted number: {value} + + ))} + + ), + }, +}; diff --git a/src/components/icon/icon.stories.tsx b/src/components/icon/icon.stories.tsx new file mode 100644 index 00000000000..c226953ff57 --- /dev/null +++ b/src/components/icon/icon.stories.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiIcon, EuiIconProps } from './icon'; + +const meta: Meta = { + title: 'Display/EuiIcon', + component: EuiIcon, + argTypes: { + color: { control: { type: 'text' } }, + }, + // Component defaults + args: { + type: 'accessibility', + size: 'm', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; diff --git a/src/components/image/image.stories.tsx b/src/components/image/image.stories.tsx new file mode 100644 index 00000000000..f2e25822d96 --- /dev/null +++ b/src/components/image/image.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiImage } from './image'; +import { EuiImageProps } from './image_types'; + +const meta: Meta = { + title: 'Display/EuiImage', + component: EuiImage, + argTypes: { + size: { control: { type: 'text' } }, + caption: { control: { type: 'text' } }, + }, + // Component defaults + args: { + size: 'original', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + src: 'https://images.unsplash.com/photo-1650253618249-fb0d32d3865c?w=900&h=400&fit=crop&q=60', + }, +}; diff --git a/src/components/inline_edit/inline_edit_text.stories.tsx b/src/components/inline_edit/inline_edit_text.stories.tsx new file mode 100644 index 00000000000..10ab035c653 --- /dev/null +++ b/src/components/inline_edit/inline_edit_text.stories.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiInlineEditText, EuiInlineEditTextProps } from './inline_edit_text'; + +const meta: Meta = { + title: 'Forms/EuiInlineEditText', + component: EuiInlineEditText, + // Component defaults + args: { + size: 'm', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + defaultValue: 'Hello World!', + inputAriaLabel: 'Edit text inline', + }, +}; + +export const EditMode: Story = { + args: { + defaultValue: 'Hello World!', + inputAriaLabel: 'Edit text inline', + startWithEditOpen: true, + }, +}; diff --git a/src/components/inline_edit/inline_edit_title.stories.tsx b/src/components/inline_edit/inline_edit_title.stories.tsx new file mode 100644 index 00000000000..4a84d6926d8 --- /dev/null +++ b/src/components/inline_edit/inline_edit_title.stories.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { + EuiInlineEditTitle, + EuiInlineEditTitleProps, +} from './inline_edit_title'; + +const meta: Meta = { + title: 'Forms/EuiInlineEditTitle', + component: EuiInlineEditTitle, + // Component defaults + args: { + size: 'm', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + heading: 'h1', + defaultValue: 'Hello World!', + inputAriaLabel: 'Edit title inline', + }, +}; + +export const EditMode: Story = { + args: { + heading: 'h1', + defaultValue: 'Hello World!', + inputAriaLabel: 'Edit title inline', + startWithEditOpen: true, + }, +}; diff --git a/src/components/inner_text/inner_text.stories.tsx b/src/components/inner_text/inner_text.stories.tsx new file mode 100644 index 00000000000..0a299d70821 --- /dev/null +++ b/src/components/inner_text/inner_text.stories.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiInnerText, EuiInnerTextProps } from './inner_text'; +import { EuiSpacer } from '../spacer'; +import { EuiCode } from '../code'; + +const meta: Meta = { + title: 'Utilities/EuiInnerText', + component: EuiInnerText, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + parameters: { + docs: { + source: { language: 'tsx' }, + }, + }, + argTypes: { + children: { control: { type: 'text' } }, + fallback: { control: { type: 'text' } }, + }, + args: { + // overwrite the type to allow for an useable playground because + // so far storybook can't handle displaying function as control input + children: 'Simple text' as unknown as any, + }, + render: ({ children, fallback }) => { + const content = children as unknown as string; + + return ( + + {(ref, innerText) => ( + <> + + {content || fallback} + + +

+ Output: +

{' '} + {innerText} + + )} +
+ ); + }, +}; diff --git a/src/components/key_pad_menu/key_pad_menu.stories.tsx b/src/components/key_pad_menu/key_pad_menu.stories.tsx new file mode 100644 index 00000000000..8f0ca82aa02 --- /dev/null +++ b/src/components/key_pad_menu/key_pad_menu.stories.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; + +import { EuiKeyPadMenu, EuiKeyPadMenuProps } from './key_pad_menu'; +import { EuiKeyPadMenuItem, EuiKeyPadMenuItemProps } from './key_pad_menu_item'; +import { EuiIcon } from '../icon'; + +const meta: Meta = { + title: 'Navigation/EuiKeyPadMenu/EuiKeyPadMenu', + component: EuiKeyPadMenu, +}; + +export default meta; +type Story = StoryObj; + +const onChange = action('onChange'); + +const StatefulKeyPadMenu = ( + props: EuiKeyPadMenuProps & { checkableType: 'single' | 'multi' } +) => { + const { children, checkableType, ...rest } = props; + const firstItem = Array.isArray(children) ? children[0] : children; + const firstItemId: string = firstItem.props?.id ?? ''; + + const [selectedItem, setSelectedItem] = useState(firstItemId); + const [selectedItems, setSelectedItems] = useState([firstItemId]); + + const handleOnChange = (id: string) => { + if (checkableType === 'single') { + setSelectedItem(id); + } else { + setSelectedItems((selectedItems): string[] => { + if (selectedItems.includes(id)) { + return selectedItems.filter((itemId) => itemId !== id); + } + + return [...selectedItems, id]; + }); + } + }; + + return ( + + {React.Children.map(children, (child) => { + if (!child) return null; + + return ( + React.isValidElement(child) && + React.cloneElement(child, { + onChange: (args: any) => { + handleOnChange(child.props.id); + onChange(args); + }, + isSelected: + checkableType === 'single' + ? selectedItem === child.props.id + : selectedItems.includes(child.props.id), + } as Partial) + ); + })} + + ); +}; + +export const Playground: Story = { + args: { + children: [ + + + , + + + , + + + , + + + , + + + , + + + , + ], + }, +}; + +export const CheckableSingle: Story = { + args: { + children: [ + + + , + + + , + + + , + + + , + + + , + + + , + ], + checkable: { + legend: 'Single checkable EuiKeyPadMenu', + }, + }, + render: (args) => , +}; + +export const CheckableMulti: Story = { + args: { + children: [ + + + , + + + , + + + , + + + , + + + , + + + , + ], + checkable: { + legend: 'Multi checkable EuiKeyPadMenu', + }, + }, + render: (args) => , +}; diff --git a/src/components/key_pad_menu/key_pad_menu_item.stories.tsx b/src/components/key_pad_menu/key_pad_menu_item.stories.tsx index 285048301b7..6a7412f45c2 100644 --- a/src/components/key_pad_menu/key_pad_menu_item.stories.tsx +++ b/src/components/key_pad_menu/key_pad_menu_item.stories.tsx @@ -14,9 +14,10 @@ import { EuiIcon } from '../icon'; import { EuiKeyPadMenuItem, EuiKeyPadMenuItemProps } from './key_pad_menu_item'; const meta: Meta = { - title: 'Navigation/EuiKeyPadMenuItem', + title: 'Navigation/EuiKeyPadMenu/EuiKeyPadMenuItem', component: EuiKeyPadMenuItem as any, argTypes: { + label: { control: { type: 'text' } }, checkable: { options: [undefined, 'multi', 'single'] }, }, args: { diff --git a/src/components/link/link.stories.tsx b/src/components/link/link.stories.tsx new file mode 100644 index 00000000000..e8ea73fa8d8 --- /dev/null +++ b/src/components/link/link.stories.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiLink, EuiLinkProps } from './link'; + +const meta: Meta = { + title: 'Navigation/EuiLink', + component: EuiLink, + argTypes: { + // setting up native HTML attributes to ensure they show up as control + target: { control: { type: 'text' } }, + rel: { control: { type: 'text' } }, + disabled: { control: { type: 'boolean' } }, + }, + args: { + color: 'primary', + type: 'button', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: 'Elastic website', + href: 'http://www.elastic.co/', + }, +}; diff --git a/src/components/list_group/list_group.stories.tsx b/src/components/list_group/list_group.stories.tsx new file mode 100644 index 00000000000..92e2db2eccf --- /dev/null +++ b/src/components/list_group/list_group.stories.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { moveStorybookControlsToCategory } from '../../../.storybook/utils'; +import { EuiListGroup, EuiListGroupProps } from './list_group'; +import { EuiListGroupItem } from './list_group_item'; + +const meta: Meta = { + title: 'Display/EuiListGroup/EuiListGroup', + component: EuiListGroup, + argTypes: moveStorybookControlsToCategory( + ['color', 'size'], + 'EuiListGroupItem props' + ), + args: { + flush: false, + bordered: false, + gutterSize: 's', + wrapText: false, + maxWidth: true, + showToolTips: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + children: [ + , + , + , + , + ], + }, +}; diff --git a/src/components/list_group/list_group_item.stories.tsx b/src/components/list_group/list_group_item.stories.tsx new file mode 100644 index 00000000000..6d733b8aada --- /dev/null +++ b/src/components/list_group/list_group_item.stories.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { disableStorybookControls } from '../../../.storybook/utils'; +import { EuiListGroupItem, EuiListGroupItemProps } from './list_group_item'; + +const meta: Meta = { + title: 'Display/EuiListGroup/EuiListGroupItem', + component: EuiListGroupItem, + argTypes: { + ...disableStorybookControls(['buttonRef']), + iconType: { + control: { type: 'text' }, + }, + }, + args: { + size: 'm', + color: 'text', + showToolTip: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + label: 'Link group item', + }, +}; diff --git a/src/components/list_group/pinnable_list_group/pinnable_list_group.stories.tsx b/src/components/list_group/pinnable_list_group/pinnable_list_group.stories.tsx new file mode 100644 index 00000000000..0b21dd5037f --- /dev/null +++ b/src/components/list_group/pinnable_list_group/pinnable_list_group.stories.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { moveStorybookControlsToCategory } from '../../../../.storybook/utils'; +import { + EuiPinnableListGroup, + EuiPinnableListGroupProps, +} from './pinnable_list_group'; + +const meta: Meta = { + title: 'Display/EuiPinnableListGroup', + component: EuiPinnableListGroup, + argTypes: moveStorybookControlsToCategory( + [ + 'bordered', + 'color', + 'flush', + 'gutterSize', + 'maxWidth', + 'showToolTips', + 'size', + 'wrapText', + ], + 'EuiListGroup props' + ), +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + listItems: [ + { + label: 'First item with pinned: true', + + pinned: true, + }, + { label: 'Second item with iconType', iconType: 'home' }, + { label: 'Third item with isActive: true', isActive: true }, + { + label: 'Fourth item with extraAction', + extraAction: { iconType: 'bell', alwaysShow: true }, + }, + ], + onPinClick: () => {}, + }, +}; diff --git a/src/components/loading/loading_chart.stories.tsx b/src/components/loading/loading_chart.stories.tsx new file mode 100644 index 00000000000..f4950997621 --- /dev/null +++ b/src/components/loading/loading_chart.stories.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiLoadingChart, EuiLoadingChartProps } from './loading_chart'; + +const meta: Meta = { + title: 'Display/EuiLoadingChart', + component: EuiLoadingChart, + args: { + size: 'm', + mono: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; diff --git a/src/components/loading/loading_elastic.stories.tsx b/src/components/loading/loading_elastic.stories.tsx new file mode 100644 index 00000000000..1c5f51ed7e8 --- /dev/null +++ b/src/components/loading/loading_elastic.stories.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiLoadingElastic, EuiLoadingElasticProps } from './loading_elastic'; + +const meta: Meta = { + title: 'Display/EuiLoadingElastic', + component: EuiLoadingElastic, + args: { + size: 'm', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; diff --git a/src/components/loading/loading_logo.stories.tsx b/src/components/loading/loading_logo.stories.tsx new file mode 100644 index 00000000000..2ac968ef78e --- /dev/null +++ b/src/components/loading/loading_logo.stories.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiLoadingLogo, EuiLoadingLogoProps } from './loading_logo'; + +const meta: Meta = { + title: 'Display/EuiLoadingLogo', + component: EuiLoadingLogo, + argTypes: { + logo: { control: { type: 'text' } }, + }, + args: { + size: 'm', + logo: 'logoKibana', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; diff --git a/src/components/loading/loading_spinner.stories.tsx b/src/components/loading/loading_spinner.stories.tsx new file mode 100644 index 00000000000..09026f23311 --- /dev/null +++ b/src/components/loading/loading_spinner.stories.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Meta, StoryObj } from '@storybook/react'; + +import { EuiLoadingSpinner, EuiLoadingSpinnerProps } from './loading_spinner'; + +const meta: Meta = { + title: 'Display/EuiLoadingSpinner', + component: EuiLoadingSpinner, + args: { + size: 'm', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {};