import { useState } from 'react' import type { Meta, StoryObj } from '@storybook/react-vite' import { IconSquareRounded } from '@tabler/icons-react' import { RichText, type RichTextBlock, type RichTextRun } from './RichText' import { InlineChip } from './InlineChip' import { Card } from './Card' const t = (text: string, ...marks: string[]): RichTextRun => ({ text, marks }) const p = (...inline: RichTextRun[]): RichTextBlock => ({ type: 'paragraph', inline }) const items = (...texts: string[]): RichTextBlock[] => texts.map((text) => ({ type: 'listItem', inline: [t(text)] })) const cell = (type: 'tableHeader' | 'tableCell', text: string): RichTextBlock => ({ type: 'unknown', typeName: type, children: [p(t(text))] }) const row = (type: 'tableHeader' | 'tableCell', ...texts: string[]): RichTextBlock => ({ type: 'unknown', typeName: 'tableRow', children: texts.map((x) => cell(type, x)) }) /** Every block the part draws, in the order a reader meets them. */ const EVERY_BLOCK: RichTextBlock[] = [ { type: 'heading', level: 1, inline: [t('A heading')] }, { type: 'heading', level: 2, inline: [t('A smaller heading')] }, { type: 'heading', level: 3, inline: [t('A third level, in the text colour')] }, p(t('Text can be '), t('bold', 'bold'), t(', '), t('italic', 'italic'), t(' or '), t('underlined', 'underline'), t(', and hold '), t('code', 'code'), t(' in a sentence.\nA line break stays a line break.')), { type: 'bulletList', children: items('An item', 'Another item') }, { type: 'orderedList', children: items('The first step', 'The second step') }, { type: 'blockquote', inline: [t('A quote, set in from a line.')] }, { type: 'codeBlock', language: 'ts', inline: [t('const item = 1 // a long line of code folds onto the next line instead of scrolling sideways')] }, { type: 'horizontalRule' }, { type: 'table', children: [row('tableHeader', 'Label', 'Value'), row('tableCell', 'Item one', 'One'), row('tableCell', 'Item two', 'Two')], }, ] const meta = { title: 'Components/RichText', component: RichText, args: { blocks: EVERY_BLOCK, size: 'default' }, argTypes: { size: { control: 'inline-radio', options: ['default', 'small'] } }, decorators: [(Story) =>
], } satisfies Meta export default meta type Story = StoryObj /** Every block: headings, text and its marks, both lists, a quote, code, a divider and a table. */ export const EveryBlock: Story = {} /** One step down, with every heading, list, quote and table kept: an item shown inside a card. */ export const Small: Story = { args: { size: 'small' }, render: (args) => ( ), } /** A link written with its own words, and a bare address, which may break anywhere. Code is never read for links. */ export const Links: Story = { args: { blocks: [ p(t('A link with its own words: [the guide](https://example.com/guide). A bare address: https://example.com/a/long/address/that/breaks/where/it/must.')), p(t('Inside code it stays text: '), t('[not a link](https://example.com)', 'code')), p(t('An '), t('underlined', 'underline'), t(' word beside [a link](https://example.com), and '), t('item.count', 'code'), t(' in a sentence.')), ], }, } /** What an app draws itself: a run that stands for an address, and words it recognises in plain text. */ export const WithTheAppsOwnParts: Story = { args: { blocks: [ p( t('Assigned to '), { text: 'nostr:npub1example', marks: [], reference: 'nostr:npub1example' }, t(', see [[Item one]] for the rest.'), ), ], renderReference: () => Name, renderText: (text) => text.split(/(\[\[[^\]]+\]\])/).map((part, i) => part.startsWith('[[') ? ( }>{part.slice(2, -2)} ) : ( part ), ), }, } /** Items indented under the one above, two levels deep, in both lists: each level is the type of the list it sits in. */ export const IndentedItems: Story = { args: { blocks: [ { type: 'bulletList', children: [ { type: 'listItem', inline: [t('Item one')] }, { type: 'listItem', indent: 1, inline: [t('Item one, under it')] }, { type: 'listItem', indent: 2, inline: [t('A level further in')] }, { type: 'listItem', indent: 1, inline: [t('Back one level')] }, { type: 'listItem', inline: [t('Item two')] }, ], }, { type: 'orderedList', children: [ { type: 'listItem', inline: [t('The first step')] }, { type: 'listItem', indent: 1, inline: [t('Each level counts from one')] }, { type: 'listItem', indent: 1, inline: [t('Then two')] }, { type: 'listItem', inline: [t('The second step')] }, ], }, ], }, } /** A block this part has no design for, from a later version: its words are drawn, never dropped. */ export const UnknownBlock: Story = { args: { blocks: [p(t('The block below is of a kind this part does not know.')), { type: 'unknown', typeName: 'later', inline: [t('Its words are still here.')] }], }, } /** A code block in a named language. */ export const CodeInALanguage: Story = { args: { blocks: [ { type: 'codeBlock', language: 'ts', inline: [ t( [ '// Count the items that are done', 'export function countDone(items: Item[]): number {', " const done = items.filter((item) => item.state === 'done')", ' return done.length + 0', '}', ].join('\n'), ), ], }, ], }, } const task = (text: string, checked?: boolean): RichTextBlock => ({ type: 'listItem', checked, inline: [t(text)] }) const callout = (tone: string, text: string): RichTextBlock => ({ type: 'blockquote', tone, inline: [t(text)] }) /** A checklist, among a plain list's items, and each callout tone after a plain quote (RIC-19). The boxes tick: `onTick` is given. */ export const ChecklistAndCallouts: Story = { render: (args) => { const [ticked, setTicked] = useState>({ c1: true }) const list = ['c1', 'c2', 'c3'].map((id, i) => ({ ...task(['Item one', 'Item two', 'Item three'][i], ticked[id] ?? false), id })) return ( setTicked((was) => ({ ...was, [id]: checked }))} blocks={[ { type: 'bulletList', children: [...list, task('A plain item keeps its bullet')] }, { type: 'blockquote', inline: [t('A plain quote.')] }, callout('note', 'Note, the default callout. A tone this part does not know is drawn as this one.'), callout('tip', 'Tip.'), callout('important', 'Important.'), callout('warning', 'Warning.'), callout('caution', 'Caution.'), ]} /> ) }, } const ticked = (text: string, checked: boolean | undefined, indent = 0): RichTextBlock => ({ ...task(text, checked), ...(indent ? { indent } : {}) }) /** * A numbered list counts its checklist items: the plain item after two of them * is 3. A sub-item sits under a ticked item, not struck through (Katerina, * review of #170), inside the ticked item's words as the editor draws it. */ export const NumberedAndNested: Story = { args: { blocks: [ { type: 'orderedList', children: [ticked('Item one', true), ticked('Item two', false), ticked('Item three, a plain item', undefined)] }, { type: 'bulletList', children: [ ticked('Item one', true), ticked('A sub-item under it', false, 1), { type: 'listItem', checked: true, inline: [t('A ticked item with '), t('[a link](https://example.com)'), t(' and '), t('code', 'code')] }, ], }, ], }, }