defmodule PhoenixKitWeb.Components.FolderExplorer do @moduledoc """ Reusable folder explorer sidebar — folder tree, navigation buttons, inline rename, and (optional) Trash / All Files / New Folder controls. Extracted from `PhoenixKitWeb.Components.MediaBrowser` so other LiveViews can embed folder navigation (folder pickers, category browsers, etc.) without duplicating the markup. ## Ownership model Pure presentation function component. The consumer owns all state and event handlers; FolderExplorer just renders. Every interactive control fires `phx-target={@myself}` back to the consumer, so the consumer must implement the relevant `handle_event/3` clauses: navigate_folder, navigate_root, navigate_view_all, toggle_folder_expand, toggle_sidebar, create_untitled_folder, start_rename_folder, rename_folder_input, rename_folder, cancel_rename_folder, toggle_trash_filter The drag-drop data attributes (`data-drop-folder`, `data-draggable-folder`, `data-drop-trash`) are present unconditionally; consumers that wire up the `MediaDragDrop` JS hook get drag-drop for free, others can ignore them. ## Usage <.folder_explorer id="my-folder-explorer" myself={@myself} folder_tree={@folder_tree} current_folder={@current_folder} expanded_folders={@expanded_folders} scope_folder_id={@scope_folder_id} scope_folder_name={@scope_folder_name} renaming_folder={@renaming_folder} renaming_source={@renaming_source} renaming_text={@renaming_text} filter_trash={@filter_trash} file_view={@file_view} sidebar_collapsed={@sidebar_collapsed} trash_count={@trash_count} /> ## Config flags - `show_create` (default `true`) — show the `+` toolbar button. - `show_all_files` (default `true`) — show the "All Files" flat-view button (only renders when `scope_folder_id` is `nil`; the flag gates that branch). - `show_trash` (default `true`) — show the Trash button + badge. Folder-color helpers (`folder_color_hex/1`, `folder_icon_style/2`, `folder_bg_style/1`) live here too since the sidebar and the grid/list folder cards in MediaBrowser both consume them. """ use PhoenixKitWeb, :html alias Phoenix.LiveView.JS # ────────────────────────────────────────────────────────────── # Top-level component # ────────────────────────────────────────────────────────────── attr :id, :string, default: "folder-explorer" attr :myself, :any, required: true attr :folder_tree, :any, required: true attr :current_folder, :any, default: nil attr :expanded_folders, :any, required: true attr :scope_folder_id, :any, default: nil attr :scope_folder_name, :string, default: "Root" attr :renaming_folder, :any, default: nil attr :renaming_source, :any, default: nil attr :renaming_text, :string, default: "" attr :filter_trash, :boolean, default: false attr :file_view, :string, default: nil attr :sidebar_collapsed, :boolean, default: false attr :trash_count, :integer, default: 0 attr :show_create, :boolean, default: true attr :show_all_files, :boolean, default: true attr :show_trash, :boolean, default: true def folder_explorer(assigns) do ~H""" """ end # ────────────────────────────────────────────────────────────── # Recursive tree node # ────────────────────────────────────────────────────────────── attr :node, :map, required: true attr :current_folder, :any, required: true attr :expanded_folders, :any, required: true attr :renaming_folder, :any, required: true attr :renaming_text, :string, default: "" attr :renaming_source, :any, required: true attr :filter_trash, :boolean, default: false attr :depth, :integer, default: 0 attr :myself, :any, required: true def folder_tree_node(assigns) do # In trash view no folder is "active" in the file sense — the user is # looking at trashed files, not a folder's contents. We keep # `@current_folder` populated in the socket so toggling trash off # restores the previous folder, but the tree highlight is suppressed # while filter_trash is on (the sidebar Trash button carries the # active highlight instead). assigns = assign( assigns, :is_active, (not assigns.filter_trash and assigns.current_folder) && assigns.current_folder.uuid == assigns.node.folder.uuid ) assigns = assign( assigns, :is_expanded, MapSet.member?(assigns.expanded_folders, assigns.node.folder.uuid) ) assigns = assign(assigns, :has_children, assigns.node.children != []) assigns = assign( assigns, :is_renaming, assigns.renaming_folder == assigns.node.folder.uuid && assigns.renaming_source == "sidebar" ) ~H"""
  • <%!-- Chevron (expand/collapse) --%> <%= if @has_children do %> <% else %> <% end %> <%= if @is_renaming do %> <%!-- Inline rename form --%>
    <.icon name="hero-folder" class="w-4 h-4 shrink-0" /> <%!-- Minimal bordered input — pairs with the row's `ring-2 ring-primary` above. Sits flush with the row's natural height (no daisyUI `input input-bordered input-xs` chunkiness) and uses a thin primary border + white bg so it reads as "edit field" without overwhelming the row. --%>
    <% else %> <%!-- Folder button (uncontrolled: phx-click instead of .link navigate) --%> <%!-- Rename button (visible on hover) --%> <% end %>
    <%!-- Children (expanded) --%> <%= if @has_children && @is_expanded do %> <% end %>
  • """ end # ────────────────────────────────────────────────────────────── # Folder color helpers (shared with grid/list folder cards) # ────────────────────────────────────────────────────────────── def folder_bg_style(color) do case folder_color_hex(color) do nil -> nil hex -> "background-color: #{hex}15" end end def folder_icon_style(color, _active? \\ false) do case folder_color_hex(color) do nil -> "color: oklch(var(--wa))" hex -> "color: #{hex}" end end def folder_color_hex("red"), do: "#ef4444" def folder_color_hex("orange"), do: "#f97316" def folder_color_hex("amber"), do: "#f59e0b" def folder_color_hex("yellow"), do: "#eab308" def folder_color_hex("lime"), do: "#84cc16" def folder_color_hex("green"), do: "#22c55e" def folder_color_hex("emerald"), do: "#10b981" def folder_color_hex("teal"), do: "#14b8a6" def folder_color_hex("cyan"), do: "#06b6d4" def folder_color_hex("sky"), do: "#0ea5e9" def folder_color_hex("blue"), do: "#3b82f6" def folder_color_hex("violet"), do: "#8b5cf6" def folder_color_hex("purple"), do: "#a855f7" def folder_color_hex("fuchsia"), do: "#d946ef" def folder_color_hex("pink"), do: "#ec4899" def folder_color_hex("rose"), do: "#f43f5e" def folder_color_hex(_), do: nil end