diff --git a/docs/docs/rich-text-formatting/basic-styles.md b/docs/docs/rich-text-formatting/basic-styles.md
deleted file mode 100644
index f5945cb02..000000000
--- a/docs/docs/rich-text-formatting/basic-styles.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-sidebar_position: 1
----
-
-# Basic styles
-
-
diff --git a/docs/docs/rich-text-formatting/basic-styles.mdx b/docs/docs/rich-text-formatting/basic-styles.mdx
new file mode 100644
index 000000000..575ceff0e
--- /dev/null
+++ b/docs/docs/rich-text-formatting/basic-styles.mdx
@@ -0,0 +1,116 @@
+---
+sidebar_position: 1
+---
+
+import InteractiveExample from '@site/src/components/InteractiveExample';
+import BasicStylesEditor from '@site/src/examples/BasicStylesEditor';
+import BasicStylesEditorSrc from '!!raw-loader!@site/src/examples/BasicStylesEditor';
+
+# Basic styles
+
+In [Your first editor](/fundamentals/your-first-editor) you wired up a single
+**Bold** button. Every other formatting style works exactly the same way: a
+`toggle` method on the `ref` changes the text, and `onChangeState` reports back
+whether the style is active. The only exceptions to that rule are **mentions**,
+**links** and **inline images**, which work a bit differently. They cannot be toggled
+on/off on any text - they require additional data to be valid, but you'll learn more
+about them in further sections. This page walks through the rest of the basic rich
+text styles and the one core distinction that shapes how they behave - **inline**
+versus **paragraph** formatting.
+
+## Inline styles
+
+Inline styles wrap a range of characters. Toggling one applies it to exactly the
+characters you have selected, or - if nothing is selected - arms it so the next
+characters you type come out styled. This is the family the bold belongs to:
+
+- **Bold** - `toggleBold()`
+- **Italic** - `toggleItalic()`
+- **Underline** - `toggleUnderline()`
+- **Strikethrough** - `toggleStrikeThrough()`
+- **Inline code** - `toggleInlineCode()`
+
+They combine freely, so a word can be bold and italic and underlined at
+once.
+
+:::note
+
+There are two more inline styles, which you cannot toggle quite like the others - **links** and **mentions**. You'll learn about them in their respective sections, once you finish this chapter.
+
+:::
+
+## Paragraph styles
+
+Paragraph styles apply to a whole paragraph at once. Toggling one affects the entire paragraph the
+cursor sits in (or every paragraph the selection touches), no matter how much of
+it is actually selected. The common ones are:
+
+- **Headings** - `toggleH1()` through `toggleH6()`
+- **Blockquote** - `toggleBlockQuote()`
+- **Code block** - `toggleCodeBlock()`
+- **Unordered list** - `toggleUnorderedList()`
+- **Ordered list** - `toggleOrderedList()`
+- **Checkbox list** - `toggleCheckboxList(checked: boolean)`
+
+:::note
+
+We will not touch list styles for now - we'll cover them in-depth in their [own section](/rich-text-formatting/lists).
+
+:::
+
+The key difference from inline styles is that **a paragraph can only have one
+paragraph style at a time**. A line can't be both a heading and a blockquote, so
+toggling a second paragraph style replaces the first rather than stacking on top.
+The editor surfaces this through the `isConflicting` flag in `onChangeState`.
+
+Code blocks go one step further: they **block** inline styles entirely. You
+can't make text bold inside a code block, and `onChangeState` reports bold as
+`isBlocking` there so you can grey the button out.
+
+:::tip
+
+The full table of what every style blocks or conflicts with is available in
+[Supported tags](/fundamentals/html-format-and-supported-tags) and the `onChangeState` behavior is described in [Style state model](/fundamentals/core-concepts#the-style-state-model).
+
+:::
+
+## Building the toolbar
+
+A toolbar with many styles is just the [first editor](/fundamentals/your-first-editor)
+loop repeated. Each button reads its slice of the `onChangeState` payload for
+`isActive` (to highlight it) and `isBlocking` (to disable it), and calls the
+matching `toggle` method on press:
+
+```tsx
+const inlineButtons = [
+ { label: 'Bold', state: state?.bold, onPress: () => ref.current?.toggleBold() },
+ { label: 'Italic', state: state?.italic, onPress: () => ref.current?.toggleItalic() },
+ { label: 'Underline', state: state?.underline, onPress: () => ref.current?.toggleUnderline() },
+ { label: 'Strike', state: state?.strikeThrough, onPress: () => ref.current?.toggleStrikeThrough() },
+];
+
+const paragraphButtons = [
+ { label: 'H1', state: state?.h1, onPress: () => ref.current?.toggleH1() },
+ { label: 'H2', state: state?.h2, onPress: () => ref.current?.toggleH2() },
+ { label: 'Quote', state: state?.blockQuote, onPress: () => ref.current?.toggleBlockQuote() },
+ { label: 'Code', state: state?.codeBlock, onPress: () => ref.current?.toggleCodeBlock() },
+];
+```
+
+## Try it out
+
+And just like that we've expanded our editor with several new styles to choose
+from a small toolbar. Select some text and toggle inline styles, or place the cursor
+on a line and try the paragraph ones - notice how switching from **H1** to **Quote**
+swaps the style instead of adding to it, and how the inline buttons grey out once
+you're inside a **Code** block.
+
+
+
+:::info
+
+This page is about _what_ each style does, using the built-in defaults. To change
+how they look - heading sizes, blockquote borders, code block colors - see
+[Styling the input](/core-functionalities/styling-the-input).
+
+:::
diff --git a/docs/docs/rich-text-formatting/inline-images.md b/docs/docs/rich-text-formatting/inline-images.md
deleted file mode 100644
index 464ec5746..000000000
--- a/docs/docs/rich-text-formatting/inline-images.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-sidebar_position: 5
----
-
-# Inline images
-
-
diff --git a/docs/docs/rich-text-formatting/inline-images.mdx b/docs/docs/rich-text-formatting/inline-images.mdx
new file mode 100644
index 000000000..ce30e32b3
--- /dev/null
+++ b/docs/docs/rich-text-formatting/inline-images.mdx
@@ -0,0 +1,61 @@
+---
+sidebar_position: 5
+---
+
+import InteractiveExample from '@site/src/components/InteractiveExample';
+import ImagesEditor from '@site/src/examples/ImagesEditor';
+import ImagesEditorSrc from '!!raw-loader!@site/src/examples/ImagesEditor';
+
+# Inline images
+
+Aside from just styled text, the library offers much more. This includes inline images - pictures embedded straight into the text flow.
+
+## Inserting an image
+
+You can insert it explicitly with the `setImage` ref method:
+
+```ts
+setImage(src: string, width: number, height: number)
+```
+
+- `src` - an absolute file path or a remote image URL.
+- `width` / `height` - the size the image should render at.
+
+The image lands on its own line at the current selection, replacing any
+selected text.
+
+```tsx
+const insertImage = () => {
+ ref.current?.setImage('https://picsum.photos/320/160', 160, 80);
+};
+```
+
+:::note
+
+You are responsible for the `width` and `height` you pass - the editor does not
+measure the image for you, so if you want to preserve a picture's aspect ratio you
+need to compute it yourself before calling `setImage`.
+
+:::
+
+## When the source can't load
+
+If the `src` can't be resolved - a broken URL, a missing file - the editor doesn't
+error out and renders a static placeholder in the image's place instead.
+
+## Try it out
+
+Place the cursor in the input and insert an image. The first button inserts a real
+photo from [Lorem Picsum](https://picsum.photos) at a matching size; the second
+points at a source that doesn't exist, so you get the fallback placeholder rather
+than a crash.
+
+
+
+:::info
+
+Images can also arrive by pasting rather than through `setImage` - the editor
+surfaces those through the `onPasteImages` event, which hands you each image's URI,
+MIME type, and dimensions so you can upload or resize before inserting.
+
+:::
diff --git a/docs/docs/rich-text-formatting/links.md b/docs/docs/rich-text-formatting/links.md
deleted file mode 100644
index 549f3ac11..000000000
--- a/docs/docs/rich-text-formatting/links.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-sidebar_position: 2
----
-
-# Links
-
-
diff --git a/docs/docs/rich-text-formatting/links.mdx b/docs/docs/rich-text-formatting/links.mdx
new file mode 100644
index 000000000..3ae976ac5
--- /dev/null
+++ b/docs/docs/rich-text-formatting/links.mdx
@@ -0,0 +1,118 @@
+---
+sidebar_position: 2
+---
+
+import InteractiveExample from '@site/src/components/InteractiveExample';
+import LinksEditor from '@site/src/examples/LinksEditor';
+import LinksEditorSrc from '!!raw-loader!@site/src/examples/LinksEditor';
+
+# Links
+
+A link is a piece of text with a URL attached to it. The editor creates links in
+two ways - **automatically**, as the user types something that looks like a URL,
+and **manually**, when you call `setLink` on a range of text. The two produce
+links that behave differently, so it's worth understanding both.
+
+## Automatic links
+
+By default the editor watches what the user types and turns anything that looks
+like a URL into a link on its own - no code required. These are **autolinks**.
+
+Because an autolink is tied to the text that produced it, it stays a link only as
+long as that text still matches. The moment the user edits the words, the pattern
+no longer holds and the link naturally breaks.
+
+### Customizing detection
+
+You control what counts as a link with the
+`linkRegex` prop. Pass your own `RegExp`
+to recognize custom patterns - for example, only `https://` URLs or a custom
+scheme that detects references like `issue-123`:
+
+```tsx
+// Detect "issue-123" style tokens as links.
+const linkRegex = /issue-\d+/g;
+
+;
+```
+
+:::note
+
+On iOS and Android the pattern is matched by the platform's native regex engine,
+so not every JavaScript regex feature is available there - variable-width
+lookbehinds, for instance, won't work. Pass `null` to turn automatic detection
+off entirely.
+
+:::
+
+## Manual links
+
+Manual links are applied explicitly with the
+`setLink` ref method:
+
+```ts
+setLink(start: number, end: number, text: string, url: string)
+```
+
+It sets a link over the range from `start` to `end`, showing `text` and pointing
+at `url`. Unlike an autolink, a manual link is a real, standalone style - the
+user can keep editing its text and it stays a link.
+
+:::tip
+
+The three positional arguments line up exactly with the
+`onChangeSelection` event payload, which reports
+the current `start`, `end`, and the `text` it spans. So "link the current
+selection" can be implemented quite simply:
+
+:::
+
+```tsx
+const [selection, setSelection] = useState(null);
+
+// ...
+ setSelection(e.nativeEvent)}
+ /* ... */
+/>;
+
+const addLink = () => {
+ if (!selection) return;
+ ref.current?.setLink(
+ selection.start,
+ selection.end,
+ selection.text,
+ 'https://swmansion.com',
+ );
+};
+```
+
+:::tip
+
+Passing a different `text` than what's selected replaces the selected text before
+applying the link. To strip a link while keeping its text, call
+`removeLink(start, end)`.
+
+:::
+
+## Try it out
+
+The editor below uses a custom `linkRegex` that autolinks `issue-123` tokens - type them and watch it become a link. Then
+select some words and hit **Link the selection** to attach a URL manually.
+Notice the difference afterward: editing the manual link's text keeps it a link,
+while editing an autolinked token breaks it.
+
+
+
+## Handling links with events
+
+There is an event that allows you to handle user's interaction with links inside `EnrichedTextInput`.
+
+- **`onLinkDetected`** fires when the cursor enters or leaves a created link.
+
+:::info
+
+This page covers creating and detecting links. To change how
+they look, see [Styling the input](/core-functionalities/styling-the-input).
+
+:::
diff --git a/docs/docs/rich-text-formatting/lists.md b/docs/docs/rich-text-formatting/lists.md
deleted file mode 100644
index 8f296a6e0..000000000
--- a/docs/docs/rich-text-formatting/lists.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-sidebar_position: 4
----
-
-# Lists
-
-
diff --git a/docs/docs/rich-text-formatting/lists.mdx b/docs/docs/rich-text-formatting/lists.mdx
new file mode 100644
index 000000000..7eed30edd
--- /dev/null
+++ b/docs/docs/rich-text-formatting/lists.mdx
@@ -0,0 +1,67 @@
+---
+sidebar_position: 4
+---
+
+import InteractiveExample from '@site/src/components/InteractiveExample';
+import ListsEditor from '@site/src/examples/ListsEditor';
+import ListsEditorSrc from '!!raw-loader!@site/src/examples/ListsEditor';
+
+# Lists
+
+Lists are [paragraph styles](/rich-text-formatting/basic-styles#paragraph-styles),
+just like headings and blockquotes - toggling one affects the whole paragraph the
+cursor sits in, or every paragraph the selection touches. The editor supports three
+kinds:
+
+- **Unordered list** - `toggleUnorderedList()`
+- **Ordered list** - `toggleOrderedList()`
+- **Checkbox list** - `toggleCheckboxList(checked: boolean)`
+
+Because they're paragraph styles, **only one can be active on a paragraph at a
+time** exactly as described in
+[Basic styles](/rich-text-formatting/basic-styles#paragraph-styles).
+
+:::note
+
+The library does not support nested lists. You can only create a single-level list. If you try to insert a nested list in any way, e.g. by calling `setValue` on the editor's `ref`, the list will get flattened by the [normalizer](/fundamentals/core-concepts#normalization).
+
+:::
+
+## Bulleted and numbered lists
+
+These two take no arguments - each call converts the affected paragraphs into a
+list, and calling it again turns them back into plain paragraphs:
+
+```tsx
+ref.current?.toggleUnorderedList();
+ref.current?.toggleOrderedList();
+```
+
+## Checkbox lists
+
+This list's items need a starting state. `toggleCheckboxList` takes a `checked` boolean that
+decides whether newly created boxes begin checked or unchecked:
+
+```tsx
+// New items start unchecked.
+ref.current?.toggleCheckboxList(false);
+```
+
+Once the list exists, the user can tick individual items by tapping their
+checkbox - you don't need to wire anything up for that.
+
+## Try it out
+
+Type a few lines, place the cursor on one (or select several), and toggle each
+list type. Notice how switching from **Bulleted** to **Numbered** swaps the style
+instead of stacking. Inserting a new line preserves the list format by creating another item, and tapping a checkbox toggles just that item.
+
+
+
+:::info
+
+This page covers creating lists. To change how they look - bullet color and size,
+marker weight, checkbox size, indentation - see
+[Styling the input](/core-functionalities/styling-the-input).
+
+:::
diff --git a/docs/docs/rich-text-formatting/mentions.md b/docs/docs/rich-text-formatting/mentions.md
deleted file mode 100644
index ee0a2d492..000000000
--- a/docs/docs/rich-text-formatting/mentions.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-sidebar_position: 3
----
-
-# Mentions
-
-
diff --git a/docs/docs/rich-text-formatting/mentions.mdx b/docs/docs/rich-text-formatting/mentions.mdx
new file mode 100644
index 000000000..2df1fd0b4
--- /dev/null
+++ b/docs/docs/rich-text-formatting/mentions.mdx
@@ -0,0 +1,98 @@
+---
+sidebar_position: 3
+---
+
+import InteractiveExample from '@site/src/components/InteractiveExample';
+import MentionEditor from '@site/src/examples/MentionEditor';
+import MentionEditorSrc from '!!raw-loader!@site/src/examples/MentionEditor';
+
+# Mentions
+
+This is a powerful feature allowing for a customizable inline style for a "mentioning" phrase - `@someone`,
+`#some-channel`, or whatever pattern fits your app. Each mention is able to carry any custom data you attach to it, so it's more than
+styled text: it's a reference you can act on later.
+
+## Mention indicators
+
+A mention begins with an **indicator** - a single character that tells the editor
+"a mention starts here". The set of recognized indicators is controlled by the
+`mentionIndicators` prop, which defaults to
+`['@']`:
+
+```tsx
+// Recognize both @user and #channel mentions.
+;
+```
+
+Typing one of these characters starts a mention. You can also start one
+programmatically with `startMention(indicator)`.
+
+## Inserting a mention with `setMention`
+
+Once a mention is being edited - the user has typed an indicator, or you've
+called `startMention` - you insert the finished mention with `setMention`:
+
+```ts
+setMention(
+ indicator: string,
+ text: string,
+ attributes?: Record,
+)
+```
+
+`setMention` replaces whatever the user has typed including the indicator with
+`text`. It also stores any `attributes` you pass - this is useful if you want to associate a mention with some data. Those attributes are preserved through the HTML, so they
+survive a round-trip through `getHTML` and `setValue`.
+
+```tsx
+const insertMention = (user: { id: string; name: string }) => {
+ ref.current?.setMention('@', `@${user.name}`, { id: user.id });
+};
+```
+
+:::note
+
+`setMention` is only operational when an actual mention preceded by an indicator is being edited. Otherwise it does nothing.
+
+:::
+
+## Try it out
+
+Type `@`, then tap the button to turn it
+into a completed `@mention`. Notice how you can type a part of a mention, e.g. `@Jo`, move the cursor inside its range and the mention insertion will properly replace it. The result is a single styled unit - try deleting one of its characters and see how it's removed.
+
+
+
+:::note
+
+A mention query spans up to two words - the editor keeps tracking the mention after one
+space, and a second space ends it. That makes `@John Doe` a valid mention.
+
+:::
+
+## Driving mentions with events
+
+This example is deliberately bare-bones - one hard-coded user behind a button.
+A real app watches what the user types after the
+indicator and shows a picker. Four events give you everything you need for
+that:
+
+- **`onStartMention`** fires when a mention starts - a good moment to open your suggestion list.
+- **`onChangeMention`** fires on every edit to the query, handing you the text
+ typed after the indicator - use it to filter the list.
+- **`onEndMention`** fires when the mention stops being edited - the cursor
+ moved away, or the query was closed - so you can dismiss the list.
+- **`onMentionDetected`** fires when the cursor enters or leaves a created mention.
+
+You'd open the list on `onStartMention`, filter it on `onChangeMention`, and
+call `setMention` when the user picks someone. This example doesn't wire up any
+of that; the full picker flow is covered end-to-end in the
+[Mention-only input](/guides/mention-only-input) guide.
+
+:::tip
+
+Prefix your custom attribute names with `data-` (e.g. `{ 'data-profile-url': user.profileUrl }`).
+Non-standard attributes can be dropped when the HTML passes through a sanitizer -
+this library's, or one on your own backend - whereas `data-*` attributes are the HTML standard for persisting custom data and are kept.
+
+:::
diff --git a/docs/docs/rich-text-formatting/text-alignment.md b/docs/docs/rich-text-formatting/text-alignment.md
deleted file mode 100644
index 1619e7a0a..000000000
--- a/docs/docs/rich-text-formatting/text-alignment.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-sidebar_position: 6
----
-
-# Text alignment
-
-
diff --git a/docs/docs/rich-text-formatting/text-alignment.mdx b/docs/docs/rich-text-formatting/text-alignment.mdx
new file mode 100644
index 000000000..c4735ea44
--- /dev/null
+++ b/docs/docs/rich-text-formatting/text-alignment.mdx
@@ -0,0 +1,45 @@
+---
+sidebar_position: 6
+---
+
+import InteractiveExample from '@site/src/components/InteractiveExample';
+import TextAlignmentEditor from '@site/src/examples/TextAlignmentEditor';
+import TextAlignmentEditorSrc from '!!raw-loader!@site/src/examples/TextAlignmentEditor';
+
+# Text alignment
+
+Alignment controls how a paragraph's text sits within the input's width. It's a
+per-paragraph property - every paragraph the selection touches gets aligned - and
+you set it with the `setTextAlignment` ref method:
+
+```ts
+setTextAlignment(alignment: 'left' | 'center' | 'right' | 'justify' | 'auto')
+```
+
+The five values are:
+
+- **`left`**, **`center`**, **`right`** - align the text to that direction.
+- **`justify`** - stretch each line to fill the full width.
+- **`auto`** - reset to the system's natural alignment.
+
+:::note
+
+On Android, `justify` is not supported. Calling `setTextAlignment('justify')` results in natural alignment (the same as `auto`).
+
+:::
+Unlike the paragraph styles from [Basic styles](/rich-text-formatting/basic-styles),
+text alignment does not report its state with the `isActive` / `isConflicting` booleans in `onChangeState`. Instead it exposes `alignment` with its relevant string value.
+
+```tsx
+const [state, setState] = useState(null);
+
+// Highlight the button that matches the paragraph at the cursor.
+const isActive = state?.alignment === 'center';
+```
+
+## Try it out
+
+Type a paragraph or two, place the cursor on one, and try each alignment. The
+active button reflects `state.alignment` for the paragraph the cursor is in.
+
+
diff --git a/docs/docs/rich-text-formatting/text-shortcuts.md b/docs/docs/rich-text-formatting/text-shortcuts.md
deleted file mode 100644
index 9b8de802b..000000000
--- a/docs/docs/rich-text-formatting/text-shortcuts.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-sidebar_position: 7
----
-
-# Text shortcuts
-
-
diff --git a/docs/docs/rich-text-formatting/text-shortcuts.mdx b/docs/docs/rich-text-formatting/text-shortcuts.mdx
new file mode 100644
index 000000000..aa8bf82dd
--- /dev/null
+++ b/docs/docs/rich-text-formatting/text-shortcuts.mdx
@@ -0,0 +1,82 @@
+---
+sidebar_position: 7
+---
+
+import InteractiveExample from '@site/src/components/InteractiveExample';
+import TextShortcutsEditor from '@site/src/examples/TextShortcutsEditor';
+import TextShortcutsEditorSrc from '!!raw-loader!@site/src/examples/TextShortcutsEditor';
+
+# Text shortcuts
+
+Text shortcuts let users format as they type, the way Markdown editors do -
+typing `#` turns a line into a heading, wrapping a word in `**` makes it bold.
+You configure them with the `textShortcuts` prop, an array that maps a `trigger`
+string to a `style`:
+
+```tsx
+;
+```
+
+The `style` values are the same rich text styles you met in
+[Basic styles](/rich-text-formatting/basic-styles) - and the inline vs. paragraph
+distinction is exactly what decides how a shortcut fires.
+
+## Paragraph vs. inline shortcuts
+
+The two families of styles trigger differently, because they format different
+things:
+
+- **Paragraph shortcuts** (`h1`–`h6`, `blockquote`, `codeblock`,
+ `unordered_list`, `ordered_list`, `checkbox_list`) fire at the **start of a
+ paragraph**. The trigger is the prefix you type before the line's content -
+ e.g. `#` for a heading, `-` for a bulleted list. Since a paragraph can only hold
+ one paragraph style, these only fire on a **plain** paragraph: if the line is
+ already a heading or a list item, typing the prefix does nothing.
+
+- **Inline shortcuts** (`bold`, `italic`, `underline`, `strikethrough`,
+ `inline_code`) fire when you type a **closing delimiter** around some text, so
+ typing `**word**` bolds `word`.
+
+:::note
+
+The usual [style rules](/rich-text-formatting/basic-styles#paragraph-styles) still
+apply to shortcut-triggered styles. If the target style is **blocked** by an
+active one (e.g. bold inside a code block), the shortcut has no effect.
+
+:::
+
+## Defaults
+
+Even without the prop, two shortcuts are active out of the box:
+
+```ts
+[
+ { trigger: '- ', style: 'unordered_list' },
+ { trigger: '1. ', style: 'ordered_list' },
+];
+```
+
+Passing your own array replaces these defaults entirely, and passing an empty
+array (`textShortcuts={[]}`) disables shortcuts altogether.
+
+## Try it out
+
+The editor below wires up one shortcut of each kind - a paragraph one (`#` → H1)
+and an inline one (`**` → bold). Start a line with `#` and watch it become a
+heading, then wrap a word in `**stars**` to bold it.
+
+
+
+:::info
+
+On Web, the same styles are also reachable through
+[keyboard shortcuts](/api-reference/enriched-text-input) like ⌘B / Ctrl+B,
+independent of the `textShortcuts` prop.
+
+:::
diff --git a/docs/src/examples/BasicStylesEditor.tsx b/docs/src/examples/BasicStylesEditor.tsx
new file mode 100644
index 000000000..8adff21ee
--- /dev/null
+++ b/docs/src/examples/BasicStylesEditor.tsx
@@ -0,0 +1,122 @@
+import { EnrichedTextInput } from 'react-native-enriched-html';
+import type {
+ EnrichedTextInputInstance,
+ OnChangeStateEvent,
+} from 'react-native-enriched-html';
+import { useRef, useState } from 'react';
+import { View, StyleSheet, Pressable, Text } from 'react-native';
+
+export default function App() {
+ const ref = useRef(null);
+ const [state, setState] = useState(null);
+
+ // Inline styles apply to the selected characters.
+ const inlineButtons = [
+ {
+ label: 'Bold',
+ state: state?.bold,
+ onPress: () => ref.current?.toggleBold(),
+ },
+ {
+ label: 'Italic',
+ state: state?.italic,
+ onPress: () => ref.current?.toggleItalic(),
+ },
+ {
+ label: 'Underline',
+ state: state?.underline,
+ onPress: () => ref.current?.toggleUnderline(),
+ },
+ {
+ label: 'Strike',
+ state: state?.strikeThrough,
+ onPress: () => ref.current?.toggleStrikeThrough(),
+ },
+ ];
+
+ // Paragraph styles apply to the whole line the cursor sits in.
+ const paragraphButtons = [
+ { label: 'H1', state: state?.h1, onPress: () => ref.current?.toggleH1() },
+ { label: 'H2', state: state?.h2, onPress: () => ref.current?.toggleH2() },
+ {
+ label: 'Quote',
+ state: state?.blockQuote,
+ onPress: () => ref.current?.toggleBlockQuote(),
+ },
+ {
+ label: 'Code',
+ state: state?.codeBlock,
+ onPress: () => ref.current?.toggleCodeBlock(),
+ },
+ ];
+
+ const renderButton = (
+ button: (typeof inlineButtons)[number] | (typeof paragraphButtons)[number]
+ ) => (
+
+
+ {button.label}
+
+
+ );
+
+ return (
+
+ setState(e.nativeEvent)}
+ />
+ {inlineButtons.map(renderButton)}
+ {paragraphButtons.map(renderButton)}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { gap: 12 },
+ input: {
+ fontSize: 18,
+ color: '#232736',
+ padding: 12,
+ borderRadius: 12,
+ minHeight: 96,
+ backgroundColor: '#eef0ff',
+ },
+ row: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ gap: 8,
+ },
+ button: {
+ width: 90,
+ padding: 8,
+ borderRadius: 24,
+ borderWidth: 1,
+ borderColor: '#919fcf',
+ },
+ buttonActive: {
+ borderColor: '#57b495',
+ backgroundColor: '#57b495',
+ },
+ buttonDisabled: {
+ opacity: 0.4,
+ },
+ text: {
+ textAlign: 'center',
+ color: '#919fcf',
+ },
+ textActive: {
+ color: '#eef0ff',
+ },
+});
diff --git a/docs/src/examples/ImagesEditor.tsx b/docs/src/examples/ImagesEditor.tsx
new file mode 100644
index 000000000..2e459522e
--- /dev/null
+++ b/docs/src/examples/ImagesEditor.tsx
@@ -0,0 +1,67 @@
+import { EnrichedTextInput } from 'react-native-enriched-html';
+import type { EnrichedTextInputInstance } from 'react-native-enriched-html';
+import { useRef } from 'react';
+import { View, StyleSheet, Pressable, Text } from 'react-native';
+
+export default function App() {
+ const ref = useRef(null);
+
+ const insertImage = () => {
+ // A real, loadable image. You supply the dimensions yourself - here we
+ // ask Lorem Picsum for a 320x160 photo and pass the proportionally down-scaled size.
+ ref.current?.setImage('https://picsum.photos/320/160', 160, 80);
+ };
+
+ const insertBrokenImage = () => {
+ // An unreachable source. The editor renders its static placeholder
+ // instead of failing, so you can see what a broken image looks like.
+ ref.current?.setImage('https://picsum.photos/does-not-exist', 160, 80);
+ };
+
+ return (
+
+
+
+
+ Insert image
+
+
+ Insert broken image
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { gap: 12 },
+ input: {
+ fontSize: 18,
+ color: '#232736',
+ padding: 12,
+ borderRadius: 12,
+ minHeight: 96,
+ backgroundColor: '#eef0ff',
+ },
+ row: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ gap: 8,
+ },
+ button: {
+ padding: 8,
+ paddingHorizontal: 16,
+ borderRadius: 24,
+ borderWidth: 1,
+ borderColor: '#919fcf',
+ },
+ text: {
+ textAlign: 'center',
+ color: '#919fcf',
+ },
+});
diff --git a/docs/src/examples/LinksEditor.tsx b/docs/src/examples/LinksEditor.tsx
new file mode 100644
index 000000000..e4d64fd94
--- /dev/null
+++ b/docs/src/examples/LinksEditor.tsx
@@ -0,0 +1,75 @@
+import { EnrichedTextInput } from 'react-native-enriched-html';
+import type {
+ EnrichedTextInputInstance,
+ OnChangeSelectionEvent,
+} from 'react-native-enriched-html';
+import { useRef, useState } from 'react';
+import { View, StyleSheet, Pressable, Text } from 'react-native';
+
+// Autolink any "issue-123" style token.
+const linkRegex = /issue-\d+/g;
+
+export default function App() {
+ const ref = useRef(null);
+ const [selection, setSelection] = useState(
+ null
+ );
+
+ const hasSelection = !!selection && selection.start !== selection.end;
+
+ const addLink = () => {
+ if (!selection) return;
+ // Turn the current selection into a link pointing at our docs.
+ ref.current?.setLink(
+ selection.start,
+ selection.end,
+ selection.text,
+ 'https://swmansion.com'
+ );
+ };
+
+ return (
+
+ setSelection(e.nativeEvent)}
+ />
+
+ Link the selection
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { gap: 12 },
+ input: {
+ fontSize: 18,
+ color: '#232736',
+ padding: 12,
+ borderRadius: 12,
+ minHeight: 96,
+ backgroundColor: '#eef0ff',
+ },
+ button: {
+ alignSelf: 'center',
+ padding: 8,
+ paddingHorizontal: 16,
+ borderRadius: 24,
+ borderWidth: 1,
+ borderColor: '#919fcf',
+ },
+ buttonDisabled: {
+ opacity: 0.4,
+ },
+ text: {
+ textAlign: 'center',
+ color: '#919fcf',
+ },
+});
diff --git a/docs/src/examples/ListsEditor.tsx b/docs/src/examples/ListsEditor.tsx
new file mode 100644
index 000000000..4336969f4
--- /dev/null
+++ b/docs/src/examples/ListsEditor.tsx
@@ -0,0 +1,100 @@
+import { EnrichedTextInput } from 'react-native-enriched-html';
+import type {
+ EnrichedTextInputInstance,
+ OnChangeStateEvent,
+} from 'react-native-enriched-html';
+import { useRef, useState } from 'react';
+import { View, StyleSheet, Pressable, Text } from 'react-native';
+
+export default function App() {
+ const ref = useRef(null);
+ const [state, setState] = useState(null);
+
+ // Lists are paragraph styles - each toggle affects whole lines, and only
+ // one list type can be active on a paragraph at a time.
+ const listButtons = [
+ {
+ label: 'Bulleted',
+ state: state?.unorderedList,
+ onPress: () => ref.current?.toggleUnorderedList(),
+ },
+ {
+ label: 'Numbered',
+ state: state?.orderedList,
+ onPress: () => ref.current?.toggleOrderedList(),
+ },
+ {
+ label: 'Checkbox',
+ state: state?.checkboxList,
+ // Pass whether new checkboxes start checked or unchecked.
+ onPress: () => ref.current?.toggleCheckboxList(false),
+ },
+ ];
+
+ const renderButton = (button: (typeof listButtons)[number]) => (
+
+
+ {button.label}
+
+
+ );
+
+ return (
+
+ setState(e.nativeEvent)}
+ />
+ {listButtons.map(renderButton)}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { gap: 12 },
+ input: {
+ fontSize: 18,
+ color: '#232736',
+ padding: 12,
+ borderRadius: 12,
+ minHeight: 96,
+ backgroundColor: '#eef0ff',
+ },
+ row: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ gap: 8,
+ },
+ button: {
+ width: 90,
+ padding: 8,
+ borderRadius: 24,
+ borderWidth: 1,
+ borderColor: '#919fcf',
+ },
+ buttonActive: {
+ borderColor: '#57b495',
+ backgroundColor: '#57b495',
+ },
+ buttonDisabled: {
+ opacity: 0.4,
+ },
+ text: {
+ textAlign: 'center',
+ color: '#919fcf',
+ },
+ textActive: {
+ color: '#eef0ff',
+ },
+});
diff --git a/docs/src/examples/MentionEditor.tsx b/docs/src/examples/MentionEditor.tsx
new file mode 100644
index 000000000..efd3e9216
--- /dev/null
+++ b/docs/src/examples/MentionEditor.tsx
@@ -0,0 +1,52 @@
+import { EnrichedTextInput } from 'react-native-enriched-html';
+import type { EnrichedTextInputInstance } from 'react-native-enriched-html';
+import { useRef } from 'react';
+import { View, StyleSheet, Pressable, Text } from 'react-native';
+
+const user = { id: '1', name: 'John' };
+
+export default function App() {
+ const ref = useRef(null);
+
+ const insertMention = () => {
+ // Replaces the active '@' mention the user has started by typing '@'.
+ ref.current?.setMention('@', `@${user.name}`, { id: user.id });
+ };
+
+ return (
+
+
+
+ @{user.name}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { gap: 12 },
+ input: {
+ fontSize: 18,
+ color: '#232736',
+ padding: 12,
+ borderRadius: 12,
+ minHeight: 96,
+ backgroundColor: '#eef0ff',
+ },
+ button: {
+ alignSelf: 'center',
+ padding: 8,
+ paddingHorizontal: 16,
+ borderRadius: 24,
+ borderWidth: 1,
+ borderColor: '#919fcf',
+ },
+ text: {
+ textAlign: 'center',
+ color: '#919fcf',
+ },
+});
diff --git a/docs/src/examples/TextAlignmentEditor.tsx b/docs/src/examples/TextAlignmentEditor.tsx
new file mode 100644
index 000000000..addc7d991
--- /dev/null
+++ b/docs/src/examples/TextAlignmentEditor.tsx
@@ -0,0 +1,77 @@
+import { EnrichedTextInput } from 'react-native-enriched-html';
+import type {
+ EnrichedTextInputInstance,
+ OnChangeStateEvent,
+} from 'react-native-enriched-html';
+import { useRef, useState } from 'react';
+import { View, StyleSheet, Pressable, Text } from 'react-native';
+
+const alignments = ['left', 'center', 'right', 'justify'] as const;
+
+export default function App() {
+ const ref = useRef(null);
+ const [state, setState] = useState(null);
+
+ const renderButton = (alignment: (typeof alignments)[number]) => {
+ // A single string reports the alignment of the paragraph at the cursor.
+ const isActive = state?.alignment === alignment;
+ return (
+ ref.current?.setTextAlignment(alignment)}>
+
+ {alignment}
+
+
+ );
+ };
+
+ return (
+
+ setState(e.nativeEvent)}
+ />
+ {alignments.map(renderButton)}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { gap: 12 },
+ input: {
+ fontSize: 18,
+ color: '#232736',
+ padding: 12,
+ borderRadius: 12,
+ minHeight: 96,
+ backgroundColor: '#eef0ff',
+ },
+ row: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ gap: 8,
+ },
+ button: {
+ width: 90,
+ padding: 8,
+ borderRadius: 24,
+ borderWidth: 1,
+ borderColor: '#919fcf',
+ },
+ buttonActive: {
+ borderColor: '#57b495',
+ backgroundColor: '#57b495',
+ },
+ text: {
+ textAlign: 'center',
+ color: '#919fcf',
+ },
+ textActive: {
+ color: '#eef0ff',
+ },
+});
diff --git a/docs/src/examples/TextShortcutsEditor.tsx b/docs/src/examples/TextShortcutsEditor.tsx
new file mode 100644
index 000000000..d3d26422b
--- /dev/null
+++ b/docs/src/examples/TextShortcutsEditor.tsx
@@ -0,0 +1,37 @@
+import { EnrichedTextInput } from 'react-native-enriched-html';
+import type { EnrichedTextInputInstance } from 'react-native-enriched-html';
+import { useRef } from 'react';
+import { View, StyleSheet } from 'react-native';
+
+export default function App() {
+ const ref = useRef(null);
+
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { gap: 12 },
+ input: {
+ fontSize: 18,
+ color: '#232736',
+ padding: 12,
+ borderRadius: 12,
+ minHeight: 96,
+ backgroundColor: '#eef0ff',
+ },
+});