# `PhiaUi.Components.Editor`
[🔗](https://github.com/charlenopires/PhiaUI/blob/v0.1.17/lib/phia_ui/components/editor/editor.ex#L1)

Editor Suite — 19 components for building rich text editing experiences.

Inspired by **TipTap** — the most popular headless rich text editor (2.5M+ weekly
npm downloads, built on ProseMirror, used by Notion, Linear, Vercel, and more) —
this module provides all the building blocks needed to assemble a full-featured editor.

## Research: Most Popular Rich Text Editors (2025)

| Editor       | Weekly DLs  | Foundation    | Used by                        |
|--------------|-------------|---------------|--------------------------------|
| **TipTap**   | 2.5M+       | ProseMirror   | Notion clones, Linear, Vercel  |
| Quill v2     | 1.8M+       | Custom        | Slack, LinkedIn, Figma, Airtable |
| Lexical      | 1.2M+       | Custom (Meta) | Facebook, WhatsApp Web         |
| ProseMirror  | 900K+       | —             | Base for TipTap/Remirror       |
| CKEditor 5   | 600K+       | Custom        | Enterprise / CMS               |

TipTap wins on DX: headless, framework-agnostic, tree-shakable, collaborative.
`advanced_editor/1` is a PhiaUI transpilation of TipTap's editor design.

## Components

### Group A — Toolbar Primitives (no JS hooks)
- `editor_toolbar/1`     — `role="toolbar"` wrapper
- `toolbar_button/1`     — single action button (active, disabled, size variants)
- `toolbar_group/1`      — `role="group"` semantic grouping
- `toolbar_separator/1`  — vertical `role="separator"` divider

### Group B — Floating / Contextual (JS hooks)
- `bubble_menu/1`         — floating toolbar above text selection (PhiaBubbleMenu)
- `floating_menu/1`       — block-insertion menu at empty cursor (PhiaFloatingMenu)
- `slash_command_menu/1`  — `/` trigger command palette (PhiaSlashCommand)

### Group C — Enhanced Inline Editing
- `inline_edit/1`         — extended editable with type/validation/buttons (PhiaEditable)
- `inline_edit_group/1`   — wrapper for bulk-editing multiple inline fields

### Group D — Rich Text Utilities
- `editor_color_picker/1`       — text/bg color palette toolbar dropdown (PhiaEditorColorPicker)
- `editor_toolbar_dropdown/1`   — generic toolbar dropdown (PhiaEditorDropdown)
- `editor_link_dialog/1`        — modal for insert/edit link
- `editor_code_block/1`         — language badge + copy button + line numbers
- `editor_character_count/1`    — char/word counter with optional progress bar
- `markdown_editor/1`           — split-pane textarea + preview (PhiaMarkdownEditor)
- `rich_text_viewer/1`          — read-only HTML container with prose sizing
- `editor_find_replace/1`       — slide-in search+replace bar (PhiaEditorFindReplace)
- `editor_word_count/1`         — words / reading-time display widget

### Bonus — TipTap-Inspired Full Editor
- `advanced_editor/1`           — complete WYSIWYG combining all primitives (PhiaAdvancedEditor)

# `advanced_editor`

Full-featured WYSIWYG editor inspired by TipTap's design.

**TipTap** is the most popular headless rich text editor (built on ProseMirror,
2.5M+ weekly npm downloads). This component transpiles TipTap's UX patterns
to PhiaUI: same toolbar layout, bubble menus, slash commands, and inline SVG icons —
powered entirely by vanilla JS + `execCommand` via the `PhiaAdvancedEditor` hook.

Features:
- Full toolbar: undo/redo, heading dropdown, marks (bold/italic/underline/strike/code),
  lists (bullet/ordered), links (insert/remove), text color picker, find & replace toggle
- Bubble menu for quick inline formatting of selected text
- Floating menu at empty cursor for block insertion
- Find & replace bar (Ctrl/Cmd+F)
- Live word/character count footer
- `Phoenix.HTML.FormField` integration via hidden `<input>`
- Dark mode support via `.dark` class

## Example

    <%!-- Standalone --%>
    <.advanced_editor id="blog-editor" placeholder="Write your post..." min_height="400px" />

    <%!-- Form-integrated --%>
    <.form for={@form} phx-submit="publish">
      <.advanced_editor id="post-editor" field={@form[:body]} show_word_count />
    </.form>

## Hook registration

    import PhiaAdvancedEditor from "./hooks/advanced_editor"
    let liveSocket = new LiveSocket("/live", Socket, {
      hooks: { PhiaAdvancedEditor, PhiaBubbleMenu, PhiaEditorColorPicker, PhiaEditorFindReplace }
    })

## Attributes

* `id` (`:string`) (required)
* `field` (`Phoenix.HTML.FormField`) - Defaults to `nil`.
* `value` (`:string`) - Defaults to `nil`.
* `placeholder` (`:string`) - Defaults to `"Type '/' for commands, or start writing..."`.
* `min_height` (`:string`) - Defaults to `"300px"`.
* `toolbar_variant` (`:atom`) - Defaults to `:default`. Must be one of `:default`, `:compact`, or `:floating`.
* `show_word_count` (`:boolean`) - Defaults to `true`.
* `show_find_replace` (`:boolean`) - Defaults to `false`.
* `class` (`:string`) - Defaults to `nil`.

# `bubble_menu`

Renders a floating toolbar that appears above the user's text selection.

The `PhiaBubbleMenu` hook listens to `selectionchange`, computes the selection
bounding rect, and positions this div above the selected text. Clamps to viewport.

## Example

    <.bubble_menu id="bubble" editor_id="my-editor">
      <.toolbar_button action="bold" aria_label="Bold" size={:xs}>
        <svg .../>
      </.toolbar_button>
    </.bubble_menu>

## Attributes

* `id` (`:string`) (required)
* `editor_id` (`:string`) (required)
* `class` (`:string`) - Defaults to `nil`.
## Slots

* `inner_block` (required)

# `editor_character_count`

Renders a server-side character/word counter with optional progress bar.

Drives via `phx-change` on the editor: the LiveView counts characters/words
and assigns `:count`. The progress bar fills and changes colour as `count` nears `max`.

## Example

    <.editor_character_count count={@char_count} max={500} mode={:characters} show_bar />
    <.editor_character_count count={@word_count} max={100} mode={:words} />
    <.editor_character_count count={@char_count} mode={:both} />

## Attributes

* `count` (`:integer`) - Defaults to `0`.
* `max` (`:integer`) - Defaults to `nil`.
* `mode` (`:atom`) - Defaults to `:characters`. Must be one of `:characters`, `:words`, or `:both`.
* `show_bar` (`:boolean`) - Defaults to `false`.
* `class` (`:string`) - Defaults to `nil`.

# `editor_code_block`

Renders a styled code block with a header bar (language badge, filename, copy button).

The copy button reuses `PhiaCopyButton` via `data-value={copy_value}`.
Pass `copy_value` with the raw code string for clipboard support.

## Example

    <.editor_code_block id="my-snippet" language="elixir" filename="hello.ex"
                        copy_value={@raw_code}>
      def hello, do: IO.puts("Hello, world!")
    </.editor_code_block>

## Attributes

* `id` (`:string`) - Defaults to `"editor-code-block"`.
* `language` (`:string`) - Defaults to `nil`.
* `filename` (`:string`) - Defaults to `nil`.
* `show_copy` (`:boolean`) - Defaults to `true`.
* `copy_value` (`:string`) - Defaults to `nil`.
* `show_line_numbers` (`:boolean`) - Defaults to `false`.
* `class` (`:string`) - Defaults to `nil`.
## Slots

* `inner_block` (required)

# `editor_color_picker`

Renders a color palette picker toolbar dropdown for text/background colors.

The `PhiaEditorColorPicker` hook dispatches `execCommand(action, false, hex)`
on swatch click and tracks the current color via `queryCommandValue` on selection change.

## Example

    <.editor_color_picker action="foreColor" label="Text color" />
    <.editor_color_picker action="hiliteColor" label="Highlight" value="#EAB308" />

## Attributes

* `id` (`:string`) - Defaults to `nil`.
* `action` (`:string`) - Defaults to `"foreColor"`.
* `label` (`:string`) - Defaults to `"Text color"`.
* `colors` (`:list`) - Defaults to `[]`.
* `value` (`:string`) - Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.

# `editor_find_replace`

Renders a slide-in search-and-replace bar for a `contenteditable` editor.

The `PhiaEditorFindReplace` hook opens on Ctrl/Cmd+F inside the editor,
injects `<mark>` elements for matches, and provides find prev/next and replace.

## Example

    <.editor_find_replace id="find-bar" editor_id="my-editor" />

## Attributes

* `id` (`:string`) (required)
* `editor_id` (`:string`) (required)
* `class` (`:string`) - Defaults to `nil`.

# `editor_link_dialog`

Renders a modal dialog for inserting or editing a hyperlink.

Fields: URL, title (optional), open-in-new-tab checkbox.
The dialog is shown/hidden programmatically via the editor hook calling
`document.getElementById(id).classList.remove('hidden')`.

## Example

    <.editor_link_dialog id="link-dialog" on_submit="insert_link" />

## Attributes

* `id` (`:string`) (required)
* `on_submit` (`:string`) - Defaults to `nil`.
* `initial_url` (`:string`) - Defaults to `nil`.
* `initial_title` (`:string`) - Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.

# `editor_toolbar`

Renders a `role="toolbar"` container for editor controls.

Variants:
- `:default` — bordered bar, wraps to multiple lines
- `:floating` — elevated popover-style (shadow, backdrop blur)
- `:compact` — minimal single-line strip

## Example

    <.editor_toolbar aria_label="Formatting toolbar">
      <.toolbar_button action="bold" aria_label="Bold">
        <svg .../>
      </.toolbar_button>
    </.editor_toolbar>

## Attributes

* `id` (`:string`) - Defaults to `nil`.
* `variant` (`:atom`) - Defaults to `:default`. Must be one of `:default`, `:floating`, or `:compact`.
* `aria_label` (`:string`) - Defaults to `"Editor toolbar"`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `inner_block` (required)

# `editor_toolbar_dropdown`

Renders a generic dropdown inside a toolbar (heading level, font size, etc.).

The `PhiaEditorDropdown` hook toggles the panel on trigger click and dispatches
the selected item's `data-action` to the linked editor.

## Example

    <.editor_toolbar_dropdown id="heading-dd" label="Paragraph">
      <:item value="paragraph" action="paragraph" label="Paragraph" />
      <:item value="h1" action="h1" label="Heading 1" />
      <:item value="h2" action="h2" label="Heading 2" />
      <:item value="h3" action="h3" label="Heading 3" />
    </.editor_toolbar_dropdown>

## Attributes

* `id` (`:string`) (required)
* `label` (`:string`) (required)
* `value` (`:string`) - Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.
## Slots

* `item` - Accepts attributes:

  * `value` (`:string`) (required)
  * `action` (`:string`)
  * `label` (`:string`) (required)

# `editor_word_count`

Renders a standalone words / characters / reading-time display widget.

All computation is server-side — pass the raw text content.

## Example

    <.editor_word_count content={@body_text} show_reading_time words_per_minute={250} />

## Attributes

* `content` (`:string`) - Defaults to `""`.
* `show_reading_time` (`:boolean`) - Defaults to `true`.
* `words_per_minute` (`:integer`) - Defaults to `200`.
* `class` (`:string`) - Defaults to `nil`.

# `floating_menu`

Renders a block-insertion menu that floats at the empty cursor line.

The `PhiaFloatingMenu` hook shows this panel when the cursor is on an empty block,
and hides it on any text input or Escape.

## Example

    <.floating_menu id="float-menu" editor_id="my-editor" trigger={:empty_line}>
      <.toolbar_button action="bulletList" aria_label="Bullet list">...</.toolbar_button>
      <.toolbar_button action="h1" aria_label="Heading 1">...</.toolbar_button>
    </.floating_menu>

## Attributes

* `id` (`:string`) (required)
* `editor_id` (`:string`) (required)
* `trigger` (`:atom`) - Defaults to `:empty_line`. Must be one of `:empty_line`, or `:slash`.
* `class` (`:string`) - Defaults to `nil`.
## Slots

* `inner_block` (required)

# `inline_edit`

Enhanced inline editable field. Extends `editable/1` with input type variants,
validation errors, loading state, and optional confirm/cancel buttons.

Uses the `PhiaEditable` hook (same as `editable/1`).

## Example

    <.inline_edit id="title-edit" value={@title} type={:text} on_submit="update_title"
                  show_buttons placeholder="Enter a title...">
      <:preview>{@title}</:preview>
    </.inline_edit>

## Attributes

* `id` (`:string`) (required)
* `value` (`:string`) - Defaults to `nil`.
* `type` (`:atom`) - Defaults to `:text`. Must be one of `:text`, `:number`, `:textarea`, or `:select`.
* `placeholder` (`:string`) - Defaults to `"Click to edit"`.
* `on_submit` (`:string`) - Defaults to `nil`.
* `on_cancel` (`:string`) - Defaults to `nil`.
* `show_buttons` (`:boolean`) - Defaults to `false`.
* `loading` (`:boolean`) - Defaults to `false`.
* `error` (`:string`) - Defaults to `nil`.
* `required` (`:boolean`) - Defaults to `false`.
* `min` (`:any`) - Defaults to `nil`.
* `max` (`:any`) - Defaults to `nil`.
* `options` (`:list`) - Defaults to `[]`.
* `class` (`:string`) - Defaults to `nil`.
## Slots

* `preview`

# `inline_edit_group`

Wrapper for grouping multiple `inline_edit/1` fields that share a bulk save/cancel flow.

Pass custom action buttons via the `:actions` slot, or use `on_save_all`/`on_cancel_all`
to auto-render default Save all / Cancel buttons.

## Example

    <.inline_edit_group id="profile-edit" on_save_all="save_profile" on_cancel_all="reset_profile">
      <.inline_edit id="name-edit" value={@name} on_submit="update_name" />
      <.inline_edit id="email-edit" value={@email} on_submit="update_email" />
    </.inline_edit_group>

## Attributes

* `id` (`:string`) (required)
* `on_save_all` (`:string`) - Defaults to `nil`.
* `on_cancel_all` (`:string`) - Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.
## Slots

* `inner_block` (required)
* `actions`

# `markdown_editor`

Renders a split-pane Markdown editor: textarea (write) + server-rendered preview.

The `PhiaMarkdownEditor` hook debounces textarea `input` events (300 ms) and
calls `pushEvent(on_change, {raw, length, words})`. The LiveView re-renders
with the updated `preview_html` after server-side Markdown parsing.

## Example

    <.markdown_editor id="body-editor" value={@draft} preview_html={@preview}
                      on_change="md_changed" label="Article body" />

## Attributes

* `id` (`:string`) (required)
* `value` (`:string`) - Defaults to `nil`.
* `preview_html` (`:string`) - Defaults to `nil`.
* `on_change` (`:string`) - Defaults to `nil`.
* `label` (`:string`) - Defaults to `nil`.
* `placeholder` (`:string`) - Defaults to `"Write in Markdown..."`.
* `min_height` (`:string`) - Defaults to `"200px"`.
* `class` (`:string`) - Defaults to `nil`.
## Slots

* `toolbar`

# `rich_text_viewer`

Renders a read-only `Phoenix.HTML.raw` container with prose sizing.

**Security:** Always sanitize `content` server-side before passing to this
component. Use `HtmlSanitizeEx` or equivalent.

## Example

    <.rich_text_viewer content={@post.body} prose_size={:lg} />

## Attributes

* `content` (`:string`) (required)
* `prose_size` (`:atom`) - Defaults to `:base`. Must be one of `:sm`, `:base`, or `:lg`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.

# `slash_command_menu`

Renders a `/` trigger command palette for block-level actions.

The `PhiaSlashCommand` hook activates when the user types `/` at the start of an
empty block. It fuzzy-filters items, supports arrow-key navigation and Enter to select.

## Example

    <.slash_command_menu id="slash" editor_id="my-editor" on_select="block_inserted">
      <:item value="h1" label="Heading 1" icon="H1" description="Large section heading" shortcut="/h1" />
      <:item value="bulletList" label="Bullet List" icon="•" description="Simple unordered list" />
      <:item value="codeBlock" label="Code Block" icon="<>" description="Insert code snippet" />
    </.slash_command_menu>

## Attributes

* `id` (`:string`) (required)
* `editor_id` (`:string`) (required)
* `on_select` (`:string`) (required)
* `class` (`:string`) - Defaults to `nil`.
## Slots

* `item` - Accepts attributes:

  * `value` (`:string`) (required)
  * `label` (`:string`) (required)
  * `icon` (`:string`)
  * `description` (`:string`)
  * `shortcut` (`:string`)

# `toolbar_button`

Renders a single toolbar action button.

The hook reads `data-action` to dispatch formatting commands and toggles
`aria-pressed` and the `is-active` CSS class automatically.

## Example

    <.toolbar_button action="bold" aria_label="Bold" tooltip="Bold (Ctrl+B)" active={@bold_active}>
      <svg class="h-4 w-4" viewBox="0 0 16 16" fill="none">
        <path d="M5 3h4.5a2.5 2.5 0 010 5H5V3zM5 8h5a2.5 2.5 0 010 5H5V8z"
              stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
      </svg>
    </.toolbar_button>

## Attributes

* `action` (`:string`) (required)
* `aria_label` (`:string`) (required)
* `active` (`:boolean`) - Defaults to `false`.
* `disabled` (`:boolean`) - Defaults to `false`.
* `tooltip` (`:string`) - Defaults to `nil`.
* `size` (`:atom`) - Defaults to `:sm`. Must be one of `:xs`, `:sm`, or `:md`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `inner_block` (required)

# `toolbar_group`

Renders a `role="group"` semantic group for related toolbar buttons.

## Example

    <.toolbar_group label="Text formatting">
      <.toolbar_button action="bold" aria_label="Bold">...</.toolbar_button>
      <.toolbar_button action="italic" aria_label="Italic">...</.toolbar_button>
    </.toolbar_group>

## Attributes

* `label` (`:string`) - Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.
## Slots

* `inner_block` (required)

# `toolbar_separator`

Renders a thin vertical `role="separator"` divider between toolbar groups.

## Example

    <.toolbar_group label="Marks">...</.toolbar_group>
    <.toolbar_separator />
    <.toolbar_group label="Blocks">...</.toolbar_group>

## Attributes

* `class` (`:string`) - Defaults to `nil`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
