orbital-discussion 0.1.1

Composable discussion and reply-thread components for Orbital
Documentation
use leptos::prelude::*;
use orbital_macros::component_doc;

/// Controlled state, [`DiscussionAdapter`] trait, and host wiring patterns.
///
/// # When to use
///
/// - Connecting a discussion thread to server actions or a custom backend adapter.
/// - Verifying [`use_discussion`] hooks against controlled reply and focus signals.
///
/// # Usage
///
/// 1. Hold replies in a controlled `Signal<Vec<DiscussionReply>>`.
/// 2. Implement [`DiscussionAdapter::submit_reply`] in your app (HTTP, server fn, etc.).
/// 3. Bridge composer submit via [`DiscussionEvents::on_submit`] — call the adapter
///    asynchronously and push the returned reply into your signal.
/// 4. Optionally pass [`DiscussionAttachmentValidation`] and wire
///    `events.on_attachment_reject` on [`DiscussionThread`] for client-side attach rules; re-validate
///    attachments in [`DiscussionAdapter::submit_reply`] before persisting.
///
/// Only `submit_reply` is required. Override [`DiscussionAdapter::fetch_branch`] when
/// your backend supports lazy branch loading (`hidden_child_count`, show-more drill-in).
/// Override [`DiscussionAdapter::upload_attachment`] when the composer should persist
/// attachment drafts to host storage before submit.
///
/// # Composer content flow
///
/// 1. User composes markdown in the composer (`DiscussionComposerFormatToolbar` optional).
/// 2. Attachment and citation drafts live in [`ComposerContext`] until submit.
/// 3. [`DiscussionEvents::on_submit`] receives [`DiscussionComposerSubmit`] with
///    `body_markdown`, `attachments`, and `citations`.
/// 4. Host adapter uploads files (if needed) and builds `DiscussionPart::File` entries
///    with real URLs — not `blob:` placeholders.
/// 5. Host adapter persists the citations array on the reply record.
/// 6. Host pushes the normalized [`DiscussionReply`] into the controlled `replies` signal.
///
/// The crate does not upload files or invent citation URLs; that remains host-owned
/// (same contract as Phase 3b adapter wiring).
///
/// # Performance
///
/// * [`DiscussionThread`](crate::DiscussionThread) builds [`DiscussionReplyGraph`] once per
///   reply-list change via a Leptos `Memo` and stores it on [`DiscussionContext::graph`](crate::DiscussionContext::graph).
/// * Custom descendants should call `ctx.graph.get()` — avoid `DiscussionReplyGraph::from_flat`
///   inside derived signals or per-row render paths.
/// * Reply lists iterate with keyed `<For>` over stable reply ids. For very large threads,
///   prefer granular subscriptions over cloning the full `replies` vector in leaf components.
///
/// # See also
///
/// * [`DiscussionLocalizationDoc`](crate::products::discussion::docs::localization::DiscussionLocalizationDoc)
/// * [`DiscussionComposerDoc`](crate::products::discussion::docs::composer::DiscussionComposerDoc)
/// * [`DiscussionEventsDoc`](crate::products::discussion::docs::events::DiscussionEventsDoc)
///
/// # Best Practices
///
/// ## Do's
///
/// * Store the adapter as `Arc<dyn DiscussionAdapter>` and clone it into async callbacks.
/// * Use `leptos::task::spawn_local` for adapter futures in CSR previews and host apps.
/// * Inspect [`DiscussionError::retryable`] before re-submitting a failed draft.
///
/// ## Don'ts
///
/// * Do not expect the crate to call your adapter automatically from the composer.
/// * Do not add HTTP or WebSocket clients inside `orbital-discussion`.
///
/// # Examples
///
/// ## Hooks + mock adapter submit
/// Controlled replies signal, `use_discussion` hook count, and in-memory adapter bridge.
/// <!-- preview -->
/// ```rust,ignore
/// use std::sync::Arc;
/// use crate::preview::fixtures::{sample_thread, PREVIEW_VIEWER_AUTHOR_ID};
/// use crate::preview::mock_adapter::MockDiscussionAdapter;
/// use crate::{DiscussionAdapter, DiscussionEvents, DiscussionFeatures, DiscussionFocus, DiscussionThread, DiscussionViewMode};
/// use leptos::prelude::*;
/// use leptos::task::spawn_local;
/// use std::collections::HashSet;
/// let (replies, set_replies) = signal(sample_thread());
/// let (focus, set_focus) = signal(DiscussionFocus::Root);
/// let (view_mode, set_view_mode) = signal(DiscussionViewMode::Tree);
/// let (collapsed, set_collapsed) = signal(HashSet::<String>::new());
/// let (reply_to, set_reply_to) = signal(None::<String>);
/// let (last_error, set_last_error) = signal(String::new());
/// let adapter: Arc<dyn DiscussionAdapter> = Arc::new(MockDiscussionAdapter::preview_viewer());
/// let current_user_id = Signal::derive(|| Some(PREVIEW_VIEWER_AUTHOR_ID.to_string()));
/// let events = DiscussionEvents {
///     on_reply_click: Some(Callback::new(move |id| set_reply_to.set(Some(id)))),
///     on_submit: Some(Callback::new({
///         let adapter = Arc::clone(&adapter);
///         move |draft| {
///             let adapter = Arc::clone(&adapter);
///             spawn_local(async move {
///                 match adapter.submit_reply(draft).await {
///                     Ok(reply) => set_replies.update(|list| list.push(reply)),
///                     Err(err) => set_last_error.set(err.message),
///                 }
///             });
///         }
///     })),
///     ..Default::default()
/// };
/// let features = DiscussionFeatures::MARKDOWN | DiscussionFeatures::FOCUS_NAVIGATION;
/// view! {
///     <div data-testid="discussion-integration-preview">
///         <span data-testid="discussion-hook-reply-count">{move || replies.get().len()}</span>
///         <pre data-testid="discussion-integration-error">{move || last_error.get()}</pre>
///         <DiscussionThread
///             replies=Signal::derive(move || replies.get())
///             focus=Signal::derive(move || focus.get())
///             set_focus=set_focus
///             view_mode=Signal::derive(move || view_mode.get())
///             set_view_mode=set_view_mode
///             collapsed=Signal::derive(move || collapsed.get())
///             set_collapsed=set_collapsed
///             reply_to=Signal::derive(move || reply_to.get())
///             set_reply_to=set_reply_to
///             current_user_id=current_user_id
///             events=events
///             features=features
///         />
///     </div>
/// }
/// ```
#[component_doc(
    category = "Discussion",
    preview_slug = "discussion-integration",
    preview_label = "Discussion Integration",
    preview_icon = icondata::AiCommentOutlined,
)]
#[component]
pub fn DiscussionIntegrationDoc() -> impl IntoView {
    view! { () }
}