defmodule PhiaUi.Components.Editor do @moduledoc """ 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) """ use Phoenix.Component import PhiaUi.ClassMerger, only: [cn: 1] # ============================================================================ # Group A — Toolbar Primitives # ============================================================================ # ---------------------------------------------------------------------------- # editor_toolbar/1 # ---------------------------------------------------------------------------- attr :id, :string, default: nil attr :variant, :atom, default: :default, values: [:default, :floating, :compact] attr :aria_label, :string, default: "Editor toolbar" attr :class, :string, default: nil attr :rest, :global slot :inner_block, required: true @doc """ 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"> """ def editor_toolbar(assigns) do ~H""" """ end defp toolbar_variant(:default), do: "flex flex-wrap items-center gap-0.5 rounded-md border border-input bg-background p-1" defp toolbar_variant(:floating), do: "flex items-center gap-0.5 rounded-lg border border-border bg-popover px-1.5 py-1 shadow-lg backdrop-blur-sm" defp toolbar_variant(:compact), do: "flex items-center gap-px rounded border border-input bg-muted/30 px-0.5 py-0.5" # ---------------------------------------------------------------------------- # toolbar_button/1 # ---------------------------------------------------------------------------- attr :action, :string, required: true attr :aria_label, :string, required: true attr :active, :boolean, default: false attr :disabled, :boolean, default: false attr :tooltip, :string, default: nil attr :size, :atom, default: :sm, values: [:xs, :sm, :md] attr :class, :string, default: nil attr :rest, :global slot :inner_block, required: true @doc """ 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}> """ def toolbar_button(assigns) do ~H""" """ end defp toolbar_btn_size(:xs), do: "h-6 w-6 p-1" defp toolbar_btn_size(:sm), do: "h-7 w-7 p-1.5" defp toolbar_btn_size(:md), do: "h-8 w-8 p-2" # ---------------------------------------------------------------------------- # toolbar_group/1 # ---------------------------------------------------------------------------- attr :label, :string, default: nil attr :class, :string, default: nil slot :inner_block, required: true @doc """ 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 action="italic" aria_label="Italic">... """ def toolbar_group(assigns) do ~H"""
{render_slot(@inner_block)}
""" end # ---------------------------------------------------------------------------- # toolbar_separator/1 # ---------------------------------------------------------------------------- attr :class, :string, default: nil @doc """ Renders a thin vertical `role="separator"` divider between toolbar groups. ## Example <.toolbar_group label="Marks">... <.toolbar_separator /> <.toolbar_group label="Blocks">... """ def toolbar_separator(assigns) do ~H"""