mirror of
https://github.com/ash-project/ash_hq.git
synced 2024-09-19 12:53:49 +12:00
Feat(DocSidebar): Extract TreeView component to improve consistency (#48)
With the TreeView component, the behaviour of each node can be controlled with options for collapsable, indent_guide, icon, link, etc. The sidebar looks roughly the same, with some improvements to spacing and some items can be collapsed now where the couldn't previously.
This commit is contained in:
parent
202e6e9f5d
commit
1265f289c0
8 changed files with 330 additions and 200 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -24,6 +24,8 @@ ash_hq-*.tar
|
||||||
|
|
||||||
# Ignore assets that are produced by build tools.
|
# Ignore assets that are produced by build tools.
|
||||||
/priv/static/assets/
|
/priv/static/assets/
|
||||||
|
/assets/css/_components.css
|
||||||
|
/assets/js/_hooks/
|
||||||
|
|
||||||
# Ignore digested assets cache.
|
# Ignore digested assets cache.
|
||||||
/priv/static/cache_manifest.json
|
/priv/static/cache_manifest.json
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
|
|
||||||
@import "syntax.css";
|
@import "syntax.css";
|
||||||
|
|
||||||
|
/* Import scoped CSS rules for components */
|
||||||
|
@import "./_components.css";
|
||||||
|
|
||||||
.search-hit {
|
.search-hit {
|
||||||
color: #fb923c;
|
color: #fb923c;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,11 @@ config :tails, colors_file: Path.join(File.cwd!(), "assets/tailwind.colors.json"
|
||||||
# Swoosh API client is needed for adapters other than SMTP.
|
# Swoosh API client is needed for adapters other than SMTP.
|
||||||
config :swoosh, :api_client, false
|
config :swoosh, :api_client, false
|
||||||
|
|
||||||
|
config :surface, :components, [
|
||||||
|
{AshHqWeb.Components.TreeView.Item, propagate_context_to_slots: true},
|
||||||
|
{AshHqWeb.Components.TreeView, propagate_context_to_slots: true}
|
||||||
|
]
|
||||||
|
|
||||||
# Configure esbuild (the version is required)
|
# Configure esbuild (the version is required)
|
||||||
config :esbuild,
|
config :esbuild,
|
||||||
version: "0.14.0",
|
version: "0.14.0",
|
||||||
|
|
|
@ -63,6 +63,7 @@ config :ash_hq, AshHqWeb.Endpoint,
|
||||||
|
|
||||||
# Watch static and templates for browser reloading.
|
# Watch static and templates for browser reloading.
|
||||||
config :ash_hq, AshHqWeb.Endpoint,
|
config :ash_hq, AshHqWeb.Endpoint,
|
||||||
|
reloadable_compilers: [:gettext, :elixir, :surface],
|
||||||
live_reload: [
|
live_reload: [
|
||||||
patterns: [
|
patterns: [
|
||||||
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||||
|
|
|
@ -3,11 +3,10 @@ defmodule AshHqWeb.Components.DocSidebar do
|
||||||
use Surface.Component
|
use Surface.Component
|
||||||
|
|
||||||
alias AshHqWeb.DocRoutes
|
alias AshHqWeb.DocRoutes
|
||||||
alias Surface.Components.LivePatch
|
alias AshHqWeb.Components.TreeView
|
||||||
|
alias AshHqWeb.Components.DocSidebarDslItems
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
|
|
||||||
import Tails
|
|
||||||
|
|
||||||
prop class, :css_class, default: ""
|
prop class, :css_class, default: ""
|
||||||
prop libraries, :list, required: true
|
prop libraries, :list, required: true
|
||||||
prop extension, :any, default: nil
|
prop extension, :any, default: nil
|
||||||
|
@ -90,240 +89,181 @@ defmodule AshHqWeb.Components.DocSidebar do
|
||||||
selected_versions={@selected_versions}
|
selected_versions={@selected_versions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-3 px-3">
|
<TreeView id={"#{@id}-treeview"}>
|
||||||
<ul class="space-y-2">
|
<TreeView.Item name="guides" text="Guides">
|
||||||
<div>
|
<TreeView.Item
|
||||||
Guides
|
:for={{category, by_library} <- @guides_by_category_and_library}
|
||||||
</div>
|
name={slug(category)}
|
||||||
{#for {category, guides_by_library} <- @guides_by_category_and_library}
|
text={category}
|
||||||
<div class="text-base-light-500">
|
collapsable
|
||||||
<button
|
class="text-base-light-500"
|
||||||
phx-click={collapse("#{@id}-#{String.replace(category, " ", "-")}")}
|
>
|
||||||
class="flex flex-row items-center"
|
<TreeView.Item
|
||||||
|
:for={{library, guides} <- by_library}
|
||||||
|
name={slug(library)}
|
||||||
|
text={library}
|
||||||
|
collapsable
|
||||||
|
class="text-base-light-500"
|
||||||
|
>
|
||||||
|
<TreeView.Item
|
||||||
|
:for={guide <- guides}
|
||||||
|
name={slug(guide.name)}
|
||||||
|
text={guide.name}
|
||||||
|
icon={render_icon(assigns, "Guide")}
|
||||||
|
selected={@guide && @guide.id == guide.id}
|
||||||
|
on_click={JS.patch(DocRoutes.doc_link(guide, @selected_versions))}
|
||||||
|
class="text-base-light-900 dark:text-base-dark-200"
|
||||||
>
|
>
|
||||||
<div id={"#{@id}-#{String.replace(category, " ", "-")}-chevron-down"}>
|
</TreeView.Item>
|
||||||
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" />
|
</TreeView.Item>
|
||||||
</div>
|
</TreeView.Item>
|
||||||
<div
|
</TreeView.Item>
|
||||||
id={"#{@id}-#{String.replace(category, " ", "-")}-chevron-right"}
|
|
||||||
class="-rotate-90"
|
|
||||||
style="display: none;"
|
|
||||||
>
|
|
||||||
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" />
|
|
||||||
</div>
|
|
||||||
<div>{category}</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id={"#{@id}-#{String.replace(category, " ", "-")}"}>
|
|
||||||
{#for {library, guides} <- guides_by_library}
|
|
||||||
<li class="ml-3 text-base-light-400 p-1">
|
|
||||||
{library}
|
|
||||||
<ul>
|
|
||||||
{#for guide <- guides}
|
|
||||||
<li class="ml-1">
|
|
||||||
<LivePatch
|
|
||||||
to={DocRoutes.doc_link(guide, @selected_versions)}
|
|
||||||
class={
|
|
||||||
"flex items-center p-1 text-base font-normal text-base-light-900 rounded-lg dark:text-base-dark-200 hover:bg-base-light-100 dark:hover:bg-base-dark-700",
|
|
||||||
"bg-base-light-300 dark:bg-base-dark-600": @guide && @guide.id == guide.id
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Heroicons.Outline.BookOpenIcon class="h-4 w-4" />
|
|
||||||
<span class="ml-3 mr-2">{guide.name}</span>
|
|
||||||
</LivePatch>
|
|
||||||
</li>
|
|
||||||
{/for}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{/for}
|
|
||||||
</div>
|
|
||||||
{/for}
|
|
||||||
<div class="mt-4">
|
|
||||||
Reference
|
|
||||||
</div>
|
|
||||||
<div class="ml-2 space-y-2">
|
|
||||||
{#if !Enum.empty?(@extensions)}
|
|
||||||
<div class="text-base-light-500">
|
|
||||||
<button phx-click={collapse("#{@id}-extension")} class="flex flex-row items-center">
|
|
||||||
<div id={"#{@id}-extension-chevron-down"}>
|
|
||||||
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" />
|
|
||||||
</div>
|
|
||||||
<div id={"#{@id}-extension-chevron-right"} class="-rotate-90" style="display: none;">
|
|
||||||
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" />
|
|
||||||
</div>
|
|
||||||
Extensions
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div id={"#{@id}-extension"}>
|
|
||||||
{#for {library, extensions} <- @extensions}
|
|
||||||
<li class="ml-3 text-base-light-200 p-1">
|
|
||||||
{library}
|
|
||||||
<ul>
|
|
||||||
{#for extension <- extensions}
|
|
||||||
<li class="ml-1">
|
|
||||||
<LivePatch
|
|
||||||
to={DocRoutes.doc_link(extension, @selected_versions)}
|
|
||||||
class="flex items-center p-1 text-base font-normal text-base-light-900 rounded-lg dark:text-base-dark-200 hover:bg-base-light-100 dark:hover:bg-base-dark-700"
|
|
||||||
>
|
|
||||||
{render_icon(assigns, extension.type)}
|
|
||||||
<span class="ml-3 mr-2">{extension.name}</span>
|
|
||||||
</LivePatch>
|
|
||||||
{#if @extension && @extension.id == extension.id && !Enum.empty?(extension.dsls)}
|
|
||||||
{render_dsls(assigns, extension.dsls, [])}
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
{/for}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{/for}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if !Enum.empty?(@mix_tasks_by_category)}
|
<TreeView.Item name="reference" text="Reference">
|
||||||
<div class="text-base-light-500">
|
<TreeView.Item
|
||||||
<button phx-click={collapse("#{@id}-mix-tasks")} class="flex flex-row items-center">
|
name="extensions"
|
||||||
<div id={"#{@id}-mix-tasks-chevron-down"}>
|
text="Extensions"
|
||||||
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" />
|
collapsable
|
||||||
</div>
|
collapsed={!@extension}
|
||||||
<div id={"#{@id}-mix-tasks-chevron-right"} class="-rotate-90" style="display: none;">
|
class="text-base-light-500"
|
||||||
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" />
|
>
|
||||||
</div>
|
<TreeView.Item
|
||||||
Mix Tasks
|
:for={{library, extensions} <- @extensions}
|
||||||
</button>
|
name={slug(library)}
|
||||||
</div>
|
text={library}
|
||||||
{/if}
|
collapsable
|
||||||
<div id={"#{@id}-mix-tasks"}>
|
class="text-base-light-400"
|
||||||
{#for {category, mix_tasks} <- @mix_tasks_by_category}
|
>
|
||||||
<div class="ml-4">
|
<TreeView.Item
|
||||||
<span class="text-sm text-base-light-900 dark:text-base-dark-100">{category}</span>
|
:for={extension <- extensions}
|
||||||
</div>
|
name={slug(extension.name)}
|
||||||
{#for mix_task <- mix_tasks}
|
text={extension.name}
|
||||||
<li class="ml-4">
|
icon={render_icon(assigns, extension.type)}
|
||||||
<LivePatch
|
collapsable={extension.dsls != []}
|
||||||
to={DocRoutes.doc_link(mix_task, @selected_versions)}
|
collapsed={!(@extension && @extension.id == extension.id)}
|
||||||
class="flex items-center space-x-2 pt-1 text-base font-normal text-base-light-900 rounded-lg dark:text-base-dark-100 hover:bg-base-light-100 dark:hover:bg-base-light-700"
|
on_click={JS.patch(DocRoutes.doc_link(extension, @selected_versions))}
|
||||||
>
|
selected={@extension && !@dsl && @extension.id == extension.id}
|
||||||
<svg
|
indent_guide
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
class="text-base-light-900 dark:text-base-dark-200"
|
||||||
fill="none"
|
>
|
||||||
viewBox="0 0 24 24"
|
<DocSidebarDslItems
|
||||||
stroke-width="1.5"
|
selected_versions={@selected_versions}
|
||||||
stroke="currentColor"
|
dsls={extension.dsls}
|
||||||
class="w-4 h-4"
|
dsl={@dsl}
|
||||||
>
|
dsl_path={[]}
|
||||||
<path
|
/>
|
||||||
stroke-linecap="round"
|
</TreeView.Item>
|
||||||
stroke-linejoin="round"
|
</TreeView.Item>
|
||||||
d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"
|
</TreeView.Item>
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="">{mix_task.name}</span>
|
|
||||||
</LivePatch>
|
|
||||||
</li>
|
|
||||||
{/for}
|
|
||||||
{/for}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-base-light-500">
|
<TreeView.Item
|
||||||
<button phx-click={collapse("#{@id}-modules")} class="flex flex-row items-center">
|
name="mix-tasks"
|
||||||
<div id={"#{@id}-modules-chevron-down"}>
|
text="Mix Tasks"
|
||||||
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" />
|
collapsable
|
||||||
</div>
|
collapsed={!@mix_task}
|
||||||
<div id={"#{@id}-modules-chevron-right"} class="-rotate-90" style="display: none;">
|
class="text-base-light-500"
|
||||||
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" />
|
>
|
||||||
</div>
|
<TreeView.Item
|
||||||
Modules
|
:for={{category, mix_tasks} <- @mix_tasks_by_category}
|
||||||
</button>
|
name={slug(category)}
|
||||||
</div>
|
text={category}
|
||||||
<div id={"#{@id}-modules"}>
|
collapsable
|
||||||
{#for {category, modules} <- @modules_by_category}
|
class="text-base-light-400"
|
||||||
<div class="ml-4">
|
>
|
||||||
<span class="text-sm text-base-light-900 dark:text-base-dark-100">{category}</span>
|
<TreeView.Item
|
||||||
</div>
|
:for={mix_task <- mix_tasks}
|
||||||
{#for module <- modules}
|
name={slug(mix_task.name)}
|
||||||
<li class="ml-4">
|
text={mix_task.name}
|
||||||
<LivePatch
|
icon={render_icon(assigns, "Mix Task")}
|
||||||
to={DocRoutes.doc_link(module, @selected_versions)}
|
on_click={JS.patch(DocRoutes.doc_link(mix_task, selected_versions))}
|
||||||
class="flex items-center space-x-2 pt-1 text-base font-normal text-base-light-900 rounded-lg dark:text-base-dark-100 hover:bg-base-light-100 dark:hover:bg-base-light-700"
|
selected={@mix_task && @mix_task.id == mix_task.id}
|
||||||
>
|
class="text-base-light-900 dark:text-base-dark-200"
|
||||||
<Heroicons.Outline.CodeIcon class="h-4 w-4" />
|
/>
|
||||||
<span class="">{module.name}</span>
|
</TreeView.Item>
|
||||||
</LivePatch>
|
</TreeView.Item>
|
||||||
</li>
|
<TreeView.Item
|
||||||
{/for}
|
name="modules"
|
||||||
{/for}
|
text="Modules"
|
||||||
</div>
|
collapsable
|
||||||
</div>
|
collapsed={!@module}
|
||||||
</ul>
|
class="text-base-light-500"
|
||||||
</div>
|
>
|
||||||
|
<TreeView.Item
|
||||||
|
:for={{category, modules} <- @modules_by_category}
|
||||||
|
name={slug(category)}
|
||||||
|
text={category}
|
||||||
|
collapsable
|
||||||
|
class="text-base-light-400"
|
||||||
|
>
|
||||||
|
<TreeView.Item
|
||||||
|
:for={module <- modules}
|
||||||
|
name={slug(module.name)}
|
||||||
|
text={module.name}
|
||||||
|
icon={render_icon(assigns, "Code")}
|
||||||
|
on_click={JS.patch(DocRoutes.doc_link(module, @selected_versions))}
|
||||||
|
selected={@module && @module.id == module.id}
|
||||||
|
class="text-base-light-900 dark:text-base-dark-200"
|
||||||
|
>
|
||||||
|
</TreeView.Item>
|
||||||
|
</TreeView.Item>
|
||||||
|
</TreeView.Item>
|
||||||
|
</TreeView.Item>
|
||||||
|
</TreeView>
|
||||||
</aside>
|
</aside>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_dsls(assigns, dsls, path) do
|
def render_icon(assigns, "Guide") do
|
||||||
~F"""
|
~F"""
|
||||||
<ul class="ml-1 flex flex-col">
|
<Heroicons.Outline.BookOpenIcon class="h-4 w-4 flex-none" />
|
||||||
{#for dsl <- Enum.filter(dsls, &(&1.path == path))}
|
|
||||||
<li class="border-l pl-1 border-primary-light-600 border-opacity-30">
|
|
||||||
<div class="flex flex-row items-center">
|
|
||||||
<LivePatch
|
|
||||||
to={DocRoutes.doc_link(dsl, @selected_versions)}
|
|
||||||
class={classes([
|
|
||||||
"flex items-center p-1 font-normal rounded-lg text-black dark:text-white hover:text-primary-light-300",
|
|
||||||
"text-primary-light-600 dark:text-primary-dark-400 font-bold":
|
|
||||||
@dsl &&
|
|
||||||
List.starts_with?(@dsl.path ++ [@dsl.name], path ++ [dsl.name])
|
|
||||||
])}
|
|
||||||
>
|
|
||||||
{dsl.name}
|
|
||||||
</LivePatch>
|
|
||||||
</div>
|
|
||||||
{render_dsls(assigns, dsls, path ++ [dsl.name])}
|
|
||||||
</li>
|
|
||||||
{/for}
|
|
||||||
</ul>
|
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_icon(assigns, "Resource") do
|
def render_icon(assigns, "Resource") do
|
||||||
~F"""
|
~F"""
|
||||||
<Heroicons.Outline.ServerIcon class="h-4 w-4" />
|
<Heroicons.Outline.ServerIcon class="h-4 w-4 flex-none" />
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_icon(assigns, "Api") do
|
def render_icon(assigns, "Api") do
|
||||||
~F"""
|
~F"""
|
||||||
<Heroicons.Outline.SwitchHorizontalIcon class="h-4 w-4" />
|
<Heroicons.Outline.SwitchHorizontalIcon class="h-4 w-4 flex-none" />
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_icon(assigns, "DataLayer") do
|
def render_icon(assigns, "DataLayer") do
|
||||||
~F"""
|
~F"""
|
||||||
<Heroicons.Outline.DatabaseIcon class="h-4 w-4" />
|
<Heroicons.Outline.DatabaseIcon class="h-4 w-4 flex-none" />
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_icon(assigns, "Flow") do
|
def render_icon(assigns, "Flow") do
|
||||||
~F"""
|
~F"""
|
||||||
<Heroicons.Outline.MapIcon class="h-4 w-4" />
|
<Heroicons.Outline.MapIcon class="h-4 w-4 flex-none" />
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_icon(assigns, "Notifier") do
|
def render_icon(assigns, "Notifier") do
|
||||||
~F"""
|
~F"""
|
||||||
<Heroicons.Outline.MailIcon class="h-4 w-4" />
|
<Heroicons.Outline.MailIcon class="h-4 w-4 flex-none" />
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_icon(assigns, "Registry") do
|
def render_icon(assigns, "Registry") do
|
||||||
~F"""
|
~F"""
|
||||||
<Heroicons.Outline.ViewListIcon class="h-4 w-4" />
|
<Heroicons.Outline.ViewListIcon class="h-4 w-4 flex-none" />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_icon(assigns, "Mix Task") do
|
||||||
|
~F"""
|
||||||
|
<Heroicons.Outline.TerminalIcon class="h-4 w-4 flex-none" />
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_icon(assigns, _) do
|
def render_icon(assigns, _) do
|
||||||
~F"""
|
~F"""
|
||||||
<Heroicons.Outline.PuzzleIcon class="h-4 w-4" />
|
<Heroicons.Outline.PuzzleIcon class="h-4 w-4 flex-none" />
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -479,10 +419,10 @@ defmodule AshHqWeb.Components.DocSidebar do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp collapse(id, js \\ %JS{}) do
|
def slug(string) do
|
||||||
js
|
string
|
||||||
|> JS.toggle(to: "##{id}", time: 0)
|
|> String.downcase()
|
||||||
|> JS.toggle(to: "##{id}-chevron-down", time: 0)
|
|> String.replace(" ", "_")
|
||||||
|> JS.toggle(to: "##{id}-chevron-right", time: 0)
|
|> String.replace(~r/[^a-z0-9-_]/, "-")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
46
lib/ash_hq_web/components/doc_sidebar_dsl_items.ex
Normal file
46
lib/ash_hq_web/components/doc_sidebar_dsl_items.ex
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
defmodule AshHqWeb.Components.DocSidebarDslItems do
|
||||||
|
@moduledoc """
|
||||||
|
Surface component for generating the recursive TreeView items for an extension DSL.
|
||||||
|
"""
|
||||||
|
use Surface.Component
|
||||||
|
alias Phoenix.LiveView.JS
|
||||||
|
|
||||||
|
alias __MODULE__
|
||||||
|
alias AshHqWeb.DocRoutes
|
||||||
|
alias AshHqWeb.Components.TreeView
|
||||||
|
alias AshHqWeb.Components.DocSidebar
|
||||||
|
|
||||||
|
@doc "List of DSLs for an extension"
|
||||||
|
prop dsls, :list, required: true
|
||||||
|
|
||||||
|
@doc "The path of the DSL to display"
|
||||||
|
prop dsl_path, :string, required: true
|
||||||
|
|
||||||
|
@doc "Selected library versions"
|
||||||
|
prop selected_versions, :map, required: true
|
||||||
|
|
||||||
|
@doc "Currently selected DSL"
|
||||||
|
prop dsl, :struct
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
~F"""
|
||||||
|
<TreeView.Item
|
||||||
|
:for={dsl <- Enum.filter(@dsls, fn dsl -> dsl.path == @dsl_path end)}
|
||||||
|
name={DocSidebar.slug(dsl.name)}
|
||||||
|
text={dsl.name}
|
||||||
|
collapsable={false}
|
||||||
|
selected={@dsl && @dsl.id == dsl.id}
|
||||||
|
on_click={JS.patch(DocRoutes.doc_link(dsl, @selected_versions))}
|
||||||
|
indent_guide
|
||||||
|
class={"pt-2": Enum.any?(@dsls, & &1.path == @dsl_path ++ [dsl.name])}
|
||||||
|
>
|
||||||
|
<DocSidebarDslItems
|
||||||
|
selected_versions={@selected_versions}
|
||||||
|
dsls={@dsls}
|
||||||
|
dsl={@dsl}
|
||||||
|
dsl_path={@dsl_path ++ [dsl.name]}
|
||||||
|
/>
|
||||||
|
</TreeView.Item>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
133
lib/ash_hq_web/components/tree_view.ex
Normal file
133
lib/ash_hq_web/components/tree_view.ex
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
defmodule AshHqWeb.Components.TreeView do
|
||||||
|
@moduledoc """
|
||||||
|
A tree view with collapsable nodes.
|
||||||
|
|
||||||
|
The component must be supplied with a list of `%Item{}` structs defining
|
||||||
|
the behaviour of each node in the tree.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Surface.Component
|
||||||
|
alias Phoenix.LiveView.JS
|
||||||
|
alias AshHqWeb.Components.TreeView.Item
|
||||||
|
|
||||||
|
@doc "DOM id for the outer div"
|
||||||
|
prop id, :string, required: true
|
||||||
|
|
||||||
|
@doc "Any additional CSS classes to add to the outer div"
|
||||||
|
prop class, :css_class
|
||||||
|
|
||||||
|
@doc "`TreeView.Item` nodes to display"
|
||||||
|
slot default
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
~F"""
|
||||||
|
<div id={@id} class={"py-3 px-3", @class}>
|
||||||
|
<ul class="space-y-4">
|
||||||
|
<#slot
|
||||||
|
:for={item <- @default}
|
||||||
|
context_put={Item, path: @id}
|
||||||
|
{item}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Item do
|
||||||
|
@moduledoc """
|
||||||
|
Data for an item in the TreeView.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Surface.Component
|
||||||
|
alias __MODULE__
|
||||||
|
|
||||||
|
prop path, :string, from_context: {Item, :path}
|
||||||
|
|
||||||
|
@doc "Logical name for the tree view item. Combined with the parent path to build the DOM id."
|
||||||
|
prop name, :string, default: nil
|
||||||
|
|
||||||
|
@doc "Text to display for the item."
|
||||||
|
prop text, :string, default: ""
|
||||||
|
|
||||||
|
@doc "Optional icon to display beside text"
|
||||||
|
prop icon, :any
|
||||||
|
|
||||||
|
@doc "Event handler to run when item clicked, eg JS.patch(~p'/some/path')"
|
||||||
|
prop on_click, :event, default: %JS{}
|
||||||
|
|
||||||
|
@doc "When true, allows the item's children to be hidden with a chevron icon."
|
||||||
|
prop collapsable, :boolean, default: false
|
||||||
|
|
||||||
|
@doc "The initial collapsed state of the children items."
|
||||||
|
prop collapsed, :boolean, default: false
|
||||||
|
|
||||||
|
@doc "The initial selection state of the item."
|
||||||
|
prop selected, :boolean, default: false
|
||||||
|
|
||||||
|
@doc "When true, displays an indentation guide to align deeply nested items."
|
||||||
|
prop indent_guide, :boolean, default: false
|
||||||
|
|
||||||
|
@doc "Any additional classes to add to the `<li>` element for the item."
|
||||||
|
prop class, :css_class, default: nil
|
||||||
|
|
||||||
|
@doc "Children `TreeView.Item` nodes."
|
||||||
|
slot default
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
~F"""
|
||||||
|
<style>
|
||||||
|
.collapsed+ul {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
.collapsed .chevron {
|
||||||
|
@apply -rotate-90;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.chevron {
|
||||||
|
@apply transition-transform duration-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.indent-guide {
|
||||||
|
@apply border-l pl-1 border-primary-light-600 border-opacity-30;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<li class={@class, "pt-2": @collapsable, "indent-guide": @indent_guide}>
|
||||||
|
<div
|
||||||
|
id={"#{@path}-#{@name}"}
|
||||||
|
class={
|
||||||
|
"rounded-lg hover:bg-base-light-100 dark:hover:bg-base-dark-700",
|
||||||
|
"bg-base-light-300 dark:bg-base-dark-600": @selected,
|
||||||
|
collapsed: @collapsed
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
:on-click={@on_click |> handle_click("#{@path}-#{@name}", @collapsable)}
|
||||||
|
class="flex flex-row items-center w-full"
|
||||||
|
>
|
||||||
|
<div :if={@collapsable && slot_assigned?(:default)} class="chevron">
|
||||||
|
<Heroicons.Outline.ChevronDownIcon class="w-3 h-3 mr-1" />
|
||||||
|
</div>
|
||||||
|
{#if @icon}{@icon}{/if}
|
||||||
|
<span class="ml-1">{@text}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul :if={slot_assigned?(:default)} class="pl-2">
|
||||||
|
<#slot context_put={Item, path: "#{@path}-#{@name}"} :for={item <- @default} {item} />
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_click(js, id, collapsable) do
|
||||||
|
if collapsable,
|
||||||
|
do: toggle_class(js, "collapsed", to: "##{id}"),
|
||||||
|
else: js
|
||||||
|
end
|
||||||
|
|
||||||
|
defp toggle_class(js, class, to: selector) do
|
||||||
|
js
|
||||||
|
|> JS.add_class(class, to: "#{selector}:not(.#{class})")
|
||||||
|
|> JS.remove_class(class, to: "#{selector}.#{class}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
2
mix.exs
2
mix.exs
|
@ -7,7 +7,7 @@ defmodule AshHq.MixProject do
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
elixir: "~> 1.12",
|
elixir: "~> 1.12",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
compilers: Mix.compilers(),
|
compilers: Mix.compilers() ++ [:surface],
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
aliases: aliases(),
|
aliases: aliases(),
|
||||||
deps: deps()
|
deps: deps()
|
||||||
|
|
Loading…
Reference in a new issue