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')] },
],
},
],
},
}