defmodule PyrauiWeb.DocsLive do use PyrauiWeb, :live_view alias PyrauiWeb.DocsLive.{ GettingStartedDocs, InstallationDocs, UsageDocs, ButtonDocs, InputDocs, CardDocs, SelectDocs, TextareaDocs, CheckboxDocs, ProgressDocs, NavbarDocs, TabsDocs, BadgeDocs, ModalDocs, TableDocs, AccordionDocs, BreadcrumbsDocs, GridDocs, ListDocs, StatsDocs, AvatarDocs, DatepickerDocs, SliderDocs, ToastDocs, StepperDocs, ChartDocs, GaugeDocs, HeatmapDocs, MapDocs, FileUploadDocs, RichTextEditorDocs, MultiSelectDocs, RatingDocs, PasswordStrengthMeterDocs, TagInputDocs, SignaturePadDocs, ColorPickerDocs, ConditionalFormDocs, CarouselDocs, WizardDocs, ResizablePanelsDocs, FilterPanelDocs, SkeletonDocs, SkeletonLayoutDocs, AutocompleteDocs, SortableListDocs, LiveChatDocs, LiveFeedDocs, CountdownDocs, KanbanDocs, GanttDocs, LiveDataTableDocs, UserProfileCardDocs, AvatarGroupDocs, ConnectionsWidgetDocs, BadgeCardDocs, ThemeSwitchDocs, AlertDocs, ThemeConfigDocs, DiffDocs, KbdDocs, LinkDocs, HeroDocs, SwapDocs, DockDocs, DrawerDocs, MockupDocs, TimelineDocs, LightboxDocs, VideoPlayerDocs, AnimatedChartDocs, MasonryGridDocs, ComingSoonDocs } @impl true def mount(params, _session, socket) do section = Map.get(params, "section", "getting-started") component = Map.get(params, "component", nil) socket = socket |> assign(section: section, component: component) |> assign(rich_text_content: nil) |> assign(form: to_form(%{"content" => nil})) |> assign(sidebar_open: true) |> assign(sidebar_section_states: build_sidebar_section_states()) |> assign(:github_star_count, "2.4k") chat_messages = live_chat_default_messages() sortable_items = sortable_list_default_items() socket = socket |> assign(:live_chat_current_user, "user-1") |> assign(:live_chat_current_user_name, "You") |> assign(:live_chat_reply_index, 0) |> assign(:"docs-chat-1_messages", chat_messages) |> assign(:"docs-chat-1_typing_users", []) |> assign(:"docs-sortable-1_items", sortable_items) {:ok, socket} end @impl true def handle_params(params, _url, socket) do if Map.has_key?(params, "section") do section = Map.get(params, "section", "getting-started") component = Map.get(params, "component", nil) {:noreply, assign(socket, section: section, component: component)} else {:noreply, socket |> assign(section: "getting-started", component: nil) |> push_navigate(to: ~p"/docs/getting-started")} end end @impl true def handle_event("validate", %{"content" => content}, socket) do {:noreply, assign(socket, rich_text_content: content)} end @impl true def handle_event("validate", _params, socket) do {:noreply, socket} end @impl true def handle_event("toggle-sidebar", _params, socket) do current = Map.get(socket.assigns, :sidebar_open, true) {:noreply, assign(socket, sidebar_open: not current)} end @impl true def handle_event("toggle-nav-section", %{"section" => section}, socket) do states = socket.assigns[:sidebar_section_states] || %{} updated_states = Map.update(states, section, true, &(!&1)) {:noreply, assign(socket, sidebar_section_states: updated_states)} end @impl true def handle_event("expand-all-sections", _params, socket) do {:noreply, assign(socket, sidebar_section_states: build_sidebar_section_states(true))} end @impl true def handle_event("collapse-all-sections", _params, socket) do {:noreply, assign(socket, sidebar_section_states: build_sidebar_section_states(false))} end @impl true def handle_event("update-rich-text", %{"content" => content}, socket) do {:noreply, assign(socket, rich_text_content: content)} end @impl true def handle_event("carousel-prev", %{"id" => id}, socket) do current = Map.get(socket.assigns, :"#{id}_current_slide", 0) new_current = max(0, current - 1) {:noreply, assign(socket, :"#{id}_current_slide", new_current)} end @impl true def handle_event("carousel-next", %{"id" => id}, socket) do current = Map.get(socket.assigns, :"#{id}_current_slide", 0) {:noreply, assign(socket, :"#{id}_current_slide", current + 1)} end @impl true def handle_event("carousel-go-to", %{"id" => id, "index" => index}, socket) do {:noreply, assign(socket, :"#{id}_current_slide", String.to_integer(index))} end @impl true def handle_event("wizard-prev", %{"id" => _id}, socket) do # Wizard navigation would need to be handled by updating step active states {:noreply, socket} end @impl true def handle_event("wizard-next", %{"id" => _id}, socket) do # Wizard navigation would need to be handled by updating step active states {:noreply, socket} end @impl true def handle_event("toggle-filter-panel", %{"id" => id}, socket) do current = Map.get(socket.assigns, :"#{id}_collapsed", false) {:noreply, assign(socket, :"#{id}_collapsed", !current)} end @impl true def handle_event("filter-changed", _params, socket) do {:noreply, socket} end @impl true def handle_event("clear-filters", %{"id" => _id}, socket) do {:noreply, socket} end @impl true def handle_event("autocomplete-search", %{"id" => id, "value" => value}, socket) do # Filter options based on search value # In a real app, this would filter from server-side data {:noreply, assign(socket, :"#{id}_search_value", value)} end @impl true def handle_event("sortable-reorder", %{"id" => id, "order" => order}, socket) do order_ids = normalize_sort_order(order) items = Map.get(socket.assigns, :"#{id}_items", []) reordered_items = reorder_sortable_items(items, order_ids) {:noreply, socket |> assign(:"#{id}_items", reordered_items) |> assign(:"#{id}_order", order_ids)} end @impl true def handle_event("chat-send", %{"id" => "docs-chat-1", "message" => message}, socket) do trimmed = message |> to_string() |> String.trim() if trimmed == "" do {:noreply, socket} else socket = socket |> append_live_chat_message("docs-chat-1", %{ text: trimmed, user_id: socket.assigns[:live_chat_current_user] || "user-1", user_name: socket.assigns[:live_chat_current_user_name] || "You" }) |> assign(:"docs-chat-1_typing_users", []) Process.send_after(self(), {:demo_chat_show_typing, "docs-chat-1"}, 400) Process.send_after(self(), {:demo_chat_reply, "docs-chat-1"}, 1_400) {:noreply, socket} end end def handle_event("chat-send", _params, socket), do: {:noreply, socket} @impl true def handle_event("chat-typing", %{"id" => "docs-chat-1"}, socket) do Process.send_after(self(), {:clear_chat_typing, "docs-chat-1"}, 1_200) {:noreply, assign(socket, :"docs-chat-1_typing_users", ["Ava Stone"])} end @impl true def handle_event("chat-typing", _params, socket), do: {:noreply, socket} @impl true def handle_event("chat-quick-reply", %{"id" => "docs-chat-1", "text" => text}, socket) do trimmed = text |> to_string() |> String.trim() if trimmed == "" do {:noreply, socket} else socket = socket |> append_live_chat_message("docs-chat-1", %{ text: trimmed, user_id: socket.assigns[:live_chat_current_user] || "user-1", user_name: socket.assigns[:live_chat_current_user_name] || "You" }) |> assign(:"docs-chat-1_typing_users", []) Process.send_after(self(), {:demo_chat_show_typing, "docs-chat-1"}, 400) Process.send_after(self(), {:demo_chat_reply, "docs-chat-1"}, 1_200) {:noreply, socket} end end @impl true def handle_event( "feed-action", %{"id" => _id, "item-id" => _item_id, "action" => _action}, socket ) do # Handle feed item action {:noreply, socket} end @impl true def handle_event("toggle-connection", %{"id" => _id}, socket) do # Handle connection toggle {:noreply, socket} end @impl true def handle_event("view-all-connections", %{"type" => _type}, socket) do # Handle view all connections {:noreply, socket} end @impl true def handle_event("toggle-drawer", %{"id" => id}, socket) do current_state = Map.get(socket.assigns, :"#{id}_open", false) {:noreply, assign(socket, :"#{id}_open", !current_state)} end @impl true def handle_event("close-drawer", %{"id" => id}, socket) do {:noreply, assign(socket, :"#{id}_open", false)} end @impl true def handle_event("open-lightbox", %{"id" => id, "index" => index}, socket) do images = Map.get(socket.assigns, :"#{id}_images", []) {:noreply, socket |> assign(:"#{id}_show", true) |> assign(:"#{id}_current_index", String.to_integer(index)) |> assign(:"#{id}_images", images)} end @impl true def handle_event("close-lightbox", %{"id" => id}, socket) do {:noreply, assign(socket, :"#{id}_show", false)} end @impl true def handle_event("lightbox-prev", %{"id" => id}, socket) do current_index = Map.get(socket.assigns, :"#{id}_current_index", 0) new_index = max(0, current_index - 1) {:noreply, assign(socket, :"#{id}_current_index", new_index)} end @impl true def handle_event("lightbox-next", %{"id" => id}, socket) do current_index = Map.get(socket.assigns, :"#{id}_current_index", 0) images = Map.get(socket.assigns, :"#{id}_images", []) new_index = min(length(images) - 1, current_index + 1) {:noreply, assign(socket, :"#{id}_current_index", new_index)} end @impl true def handle_event("open-modal", %{"id" => id}, socket) do {:noreply, assign(socket, String.to_atom("#{id}_open"), true)} end @impl true def handle_event("close-modal", %{"id" => id}, socket) do {:noreply, assign(socket, String.to_atom("#{id}_open"), false)} end @impl true def handle_info({:demo_chat_show_typing, "docs-chat-1"}, socket) do {:noreply, assign(socket, :"docs-chat-1_typing_users", ["Ava Stone"])} end @impl true def handle_info({:clear_chat_typing, "docs-chat-1"}, socket) do {:noreply, assign(socket, :"docs-chat-1_typing_users", [])} end @impl true def handle_info({:demo_chat_reply, "docs-chat-1"}, socket) do replies = demo_chat_replies() reply_idx = Map.get(socket.assigns, :live_chat_reply_index, 0) reply = Enum.at(replies, rem(reply_idx, max(length(replies), 1))) socket = socket |> assign(:live_chat_reply_index, reply_idx + 1) |> assign(:"docs-chat-1_typing_users", []) |> append_live_chat_message("docs-chat-1", %{ text: reply, user_id: "ava", user_name: "Ava Stone" }) {:noreply, socket} end @impl true def handle_info(_msg, socket), do: {:noreply, socket} @impl true def render(assigns) do ~H"""
<.link navigate={~p"/"} class="flex items-center gap-4 text-sm font-semibold uppercase tracking-[0.35em] text-white/90 transition hover:text-white" >
PyraUI logo
PyraUI
{@github_star_count} ⭐ Playground
<%= if @sidebar_open do %> <% end %>
{render_section(assigns)}
""" end defp navigation_sections_raw do [ {"Getting Started", [ %{label: "Introduction", path: "getting-started"}, %{label: "Installation", path: "installation"}, %{label: "Usage", path: "usage"} ]}, {"Layout & Navigation", [ %{label: "Navbar", path: "navbar"}, %{label: "Tabs", path: "tabs"}, %{label: "Breadcrumbs", path: "breadcrumbs"}, %{label: "Accordion", path: "accordion"}, %{label: "Grid", path: "grid"} ]}, {"Data Display", [ %{label: "Table", path: "table"}, %{label: "List", path: "list"}, %{label: "Badge", path: "badge"}, %{label: "Stats", path: "stats"}, %{label: "Avatar", path: "avatar"} ]}, {"Forms", [ %{label: "Input", path: "input"}, %{label: "Select", path: "select"}, %{label: "DatePicker", path: "datepicker"}, %{label: "Slider", path: "slider"}, %{label: "File Upload", path: "file-upload"}, %{label: "Rich Text Editor", path: "rich-text-editor"}, %{label: "Multi-Select", path: "multi-select"}, %{label: "Rating", path: "rating"}, %{label: "Password Strength Meter", path: "password-strength-meter"}, %{label: "Tag Input", path: "tag-input"}, %{label: "Signature Pad", path: "signature-pad"}, %{label: "Color Picker", path: "color-picker"}, %{label: "Conditional Form", path: "conditional-form"} ]}, {"Feedback", [ %{label: "Alert", path: "alert"}, %{label: "Modal", path: "modal"}, %{label: "Toast", path: "toast"}, %{label: "Progress", path: "progress"} ]}, {"Utilities", [ %{label: "Button", path: "button"}, %{label: "Card", path: "card"}, %{label: "Stepper", path: "stepper"} ]}, {"Layout & Utilities", [ %{label: "Carousel", path: "carousel"}, %{label: "Wizard", path: "wizard"}, %{label: "Resizable Panels", path: "resizable-panels"}, %{label: "Filter Panel", path: "filter-panel"}, %{label: "Skeleton", path: "skeleton"} ]}, {"Interactive", [ %{label: "Autocomplete", path: "autocomplete"}, %{label: "Sortable List", path: "sortable-list"}, %{label: "Live Chat", path: "live-chat"}, %{label: "Live Feed", path: "live-feed"}, %{label: "Countdown", path: "countdown"}, %{label: "Kanban Board", path: "kanban"}, %{label: "Gantt Chart", path: "gantt"}, %{label: "Live Data Table", path: "live-data-table"} ]}, {"User & Social", [ %{label: "User Profile Card", path: "user-profile-card"}, %{label: "Avatar Group", path: "avatar-group"}, %{label: "Connections Widget", path: "connections-widget"}, %{label: "Badge Card", path: "badge-card"} ]}, {"Accessibility & Theming", [ %{label: "Theme Switch", path: "theme-switch"}, %{label: "Alert", path: "alert"}, %{label: "Theme Config", path: "theme-config"} ]}, {"Miscellaneous", [ %{label: "Diff", path: "diff"}, %{label: "Kbd", path: "kbd"}, %{label: "Link", path: "link"}, %{label: "Hero", path: "hero"}, %{label: "Swap", path: "swap"}, %{label: "Dock", path: "dock"}, %{label: "Drawer", path: "drawer"}, %{label: "Mockup", path: "mockup"}, %{label: "Timeline", path: "timeline"} ]}, {"Visual & Presentation", [ %{label: "Lightbox", path: "lightbox"}, %{label: "Video Player", path: "video-player"}, %{label: "Animated Chart", path: "animated-chart"}, %{label: "Skeleton Layout", path: "skeleton-layout"}, %{label: "Masonry Grid", path: "masonry-grid"} ]}, {"Data & Analytics", [ %{label: "Chart", path: "chart"}, %{label: "Gauge", path: "gauge"}, %{label: "Heatmap", path: "heatmap"}, %{label: "Map", path: "map"} ]} ] end defp navigation_sections do navigation_sections_raw() |> Enum.map(fn {title, items} -> { title, items |> Enum.sort_by(&String.downcase(&1.label)) } end) |> Enum.sort_by(fn {title, _items} -> String.downcase(title) end) end defp build_sidebar_section_states(default \\ true) do navigation_sections() |> Enum.map(fn {title, _items} -> {title, default} end) |> Map.new() end defp render_section(assigns) do case assigns.section do "getting-started" -> GettingStartedDocs.render(assigns) "installation" -> InstallationDocs.render(assigns) "usage" -> UsageDocs.render(assigns) "button" -> ButtonDocs.render(assigns) "input" -> InputDocs.render(assigns) "card" -> CardDocs.render(assigns) "alert" -> AlertDocs.render(assigns) "select" -> SelectDocs.render(assigns) "textarea" -> TextareaDocs.render(assigns) "checkbox" -> CheckboxDocs.render(assigns) "progress" -> ProgressDocs.render(assigns) "navbar" -> NavbarDocs.render(assigns) "tabs" -> TabsDocs.render(assigns) "badge" -> BadgeDocs.render(assigns) "modal" -> ModalDocs.render(assigns) "table" -> TableDocs.render(assigns) "accordion" -> AccordionDocs.render(assigns) "breadcrumbs" -> BreadcrumbsDocs.render(assigns) "grid" -> GridDocs.render(assigns) "list" -> ListDocs.render(assigns) "stats" -> StatsDocs.render(assigns) "avatar" -> AvatarDocs.render(assigns) "datepicker" -> DatepickerDocs.render(assigns) "slider" -> SliderDocs.render(assigns) "toast" -> ToastDocs.render(assigns) "stepper" -> StepperDocs.render(assigns) "chart" -> ChartDocs.render(assigns) "gauge" -> GaugeDocs.render(assigns) "heatmap" -> HeatmapDocs.render(assigns) "map" -> MapDocs.render(assigns) "file-upload" -> FileUploadDocs.render(assigns) "rich-text-editor" -> RichTextEditorDocs.render(assigns) "multi-select" -> MultiSelectDocs.render(assigns) "rating" -> RatingDocs.render(assigns) "password-strength-meter" -> PasswordStrengthMeterDocs.render(assigns) "tag-input" -> TagInputDocs.render(assigns) "signature-pad" -> SignaturePadDocs.render(assigns) "color-picker" -> ColorPickerDocs.render(assigns) "conditional-form" -> ConditionalFormDocs.render(assigns) "carousel" -> CarouselDocs.render(assigns) "wizard" -> WizardDocs.render(assigns) "resizable-panels" -> ResizablePanelsDocs.render(assigns) "filter-panel" -> FilterPanelDocs.render(assigns) "skeleton" -> SkeletonDocs.render(assigns) "autocomplete" -> AutocompleteDocs.render(assigns) "sortable-list" -> SortableListDocs.render(assigns) "live-chat" -> LiveChatDocs.render(assigns) "live-feed" -> LiveFeedDocs.render(assigns) "countdown" -> CountdownDocs.render(assigns) "kanban" -> KanbanDocs.render(assigns) "gantt" -> GanttDocs.render(assigns) "live-data-table" -> LiveDataTableDocs.render(assigns) "user-profile-card" -> UserProfileCardDocs.render(assigns) "avatar-group" -> AvatarGroupDocs.render(assigns) "connections-widget" -> ConnectionsWidgetDocs.render(assigns) "badge-card" -> BadgeCardDocs.render(assigns) "theme-switch" -> ThemeSwitchDocs.render(assigns) "theme-config" -> ThemeConfigDocs.render(assigns) "diff" -> DiffDocs.render(assigns) "kbd" -> KbdDocs.render(assigns) "link" -> LinkDocs.render(assigns) "hero" -> HeroDocs.render(assigns) "swap" -> SwapDocs.render(assigns) "dock" -> DockDocs.render(assigns) "drawer" -> DrawerDocs.render(assigns) "mockup" -> MockupDocs.render(assigns) "timeline" -> TimelineDocs.render(assigns) "lightbox" -> LightboxDocs.render(assigns) "video-player" -> VideoPlayerDocs.render(assigns) "animated-chart" -> AnimatedChartDocs.render(assigns) "skeleton-layout" -> SkeletonLayoutDocs.render(assigns) "masonry-grid" -> MasonryGridDocs.render(assigns) _ -> ComingSoonDocs.render(assigns) end end defp content_wrapper_classes(section) when section in ["alert", "theme-config", "accordion"] do "w-full px-4 sm:px-8 md:px-12 lg:px-16 xl:px-20 2xl:px-24 py-12" end defp content_wrapper_classes(_section) do "max-w-4xl mx-auto px-8 py-12 text-gray-900" end defp main_wrapper_classes(section) when section in ["alert", "theme-config", "accordion"] do "flex-1 overflow-y-auto bg-slate-950" end defp main_wrapper_classes(_section) do "flex-1 overflow-y-auto bg-white" end defp live_chat_default_messages do now = DateTime.utc_now() [ %{ user_id: "ava", user_name: "Ava Stone", text: "Welcome to PyraUI live support! 👋", timestamp: DateTime.add(now, -600, :second) }, %{ user_id: "ava", user_name: "Ava Stone", text: "Drop your question—I'll reply in real time with polished patterns.", timestamp: DateTime.add(now, -540, :second) }, %{ user_id: "user-1", user_name: "You", text: "Hey Ava! I'm trying to theme the dashboard chat area.", timestamp: DateTime.add(now, -420, :second) }, %{ user_id: "ava", user_name: "Ava Stone", text: "Love it. Are you targeting a light or glow surface?", timestamp: DateTime.add(now, -360, :second) } ] end defp append_live_chat_message(socket, id, attrs) do text = attrs |> Map.get(:text, "") |> to_string() timestamp = Map.get(attrs, :timestamp, DateTime.utc_now()) message = attrs |> Map.put(:text, text) |> Map.put(:timestamp, timestamp) messages = Map.get(socket.assigns, :"#{id}_messages", live_chat_default_messages()) assign(socket, :"#{id}_messages", messages ++ [message]) end defp demo_chat_replies do [ "Here’s a gradient-ready header you can drop into the chat shell ✨", "Pair glass surfaces with a subtle slate border for premium depth.", "Streaming the message list keeps transitions butter-smooth on mobile.", "Use the toolbar slot for quick macros—perfect for handoff flows.", "Toggle dark mode by flipping the gradient to indigo-950 with frosted overlay.", "I can also share a Tailwind token set if you want to reuse this palette." ] end defp sortable_list_default_items do [ %{ id: "priority-roadmap", title: "Prioritized roadmap", description: "Reorder launch milestones and share with leadership." }, %{ id: "segment-handoff", title: "Segment handoff", description: "Drag customers into success pods for automated rituals." }, %{ id: "design-review", title: "Design QA checklist", description: "Guarantee every ship hits the visual + UX spec." }, %{ id: "founder-updates", title: "Founder update queue", description: "Keep exec updates crisp with drag-and-drop talking points." } ] end defp normalize_sort_order(order) when is_list(order) do order |> Enum.map(&to_string/1) |> Enum.reject(&(&1 == "")) end defp normalize_sort_order(order) when is_binary(order) do order |> String.split([",", " "], trim: true) |> Enum.reject(&(&1 == "")) end defp normalize_sort_order(_), do: [] defp reorder_sortable_items(items, order_ids) do id_map = Enum.reduce(items, %{}, fn item, acc -> id = item |> Map.get(:id) || Map.get(item, "id") |> to_string() Map.put(acc, id, item) end) ordered = order_ids |> Enum.map(&Map.get(id_map, to_string(&1))) |> Enum.reject(&is_nil/1) leftovers = items -- ordered ordered ++ leftovers end end