diff --git a/.gitignore b/.gitignore
index fed951f..3d4f3ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,8 @@ ash_hq-*.tar
# Ignore assets that are produced by build tools.
/priv/static/assets/
+/assets/css/_components.css
+/assets/js/_hooks/
# Ignore digested assets cache.
/priv/static/cache_manifest.json
diff --git a/assets/css/app.css b/assets/css/app.css
index 64eb387..f96d501 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -5,6 +5,9 @@
@import "syntax.css";
+/* Import scoped CSS rules for components */
+@import "./_components.css";
+
.search-hit {
color: #fb923c;
}
diff --git a/config/config.exs b/config/config.exs
index e730afb..1d93e4d 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -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.
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)
config :esbuild,
version: "0.14.0",
diff --git a/config/dev.exs b/config/dev.exs
index b477ec7..03d6772 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -63,6 +63,7 @@ config :ash_hq, AshHqWeb.Endpoint,
# Watch static and templates for browser reloading.
config :ash_hq, AshHqWeb.Endpoint,
+ reloadable_compilers: [:gettext, :elixir, :surface],
live_reload: [
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
diff --git a/lib/ash_hq_web/components/doc_sidebar.ex b/lib/ash_hq_web/components/doc_sidebar.ex
index c493a33..fe3f96d 100644
--- a/lib/ash_hq_web/components/doc_sidebar.ex
+++ b/lib/ash_hq_web/components/doc_sidebar.ex
@@ -3,11 +3,10 @@ defmodule AshHqWeb.Components.DocSidebar do
use Surface.Component
alias AshHqWeb.DocRoutes
- alias Surface.Components.LivePatch
+ alias AshHqWeb.Components.TreeView
+ alias AshHqWeb.Components.DocSidebarDslItems
alias Phoenix.LiveView.JS
- import Tails
-
prop class, :css_class, default: ""
prop libraries, :list, required: true
prop extension, :any, default: nil
@@ -90,240 +89,181 @@ defmodule AshHqWeb.Components.DocSidebar do
selected_versions={@selected_versions}
/>
-
-
-
- Guides
-
- {#for {category, guides_by_library} <- @guides_by_category_and_library}
-
-
+
+
+
+
-
-
-
-
-
-
- {category}
-
-
-
- {#for {library, guides} <- guides_by_library}
-
- {library}
-
- {#for guide <- guides}
-
-
-
- {guide.name}
-
-
- {/for}
-
-
- {/for}
-
- {/for}
-
- Reference
-
-
- {#if !Enum.empty?(@extensions)}
-
-
-
-
-
-
-
-
- Extensions
-
-
- {/if}
-
- {#for {library, extensions} <- @extensions}
-
- {library}
-
- {#for extension <- extensions}
-
-
- {render_icon(assigns, extension.type)}
- {extension.name}
-
- {#if @extension && @extension.id == extension.id && !Enum.empty?(extension.dsls)}
- {render_dsls(assigns, extension.dsls, [])}
- {/if}
-
- {/for}
-
-
- {/for}
-
+
+
+
+
- {#if !Enum.empty?(@mix_tasks_by_category)}
-
-
-
-
-
-
-
-
- Mix Tasks
-
-
- {/if}
-
- {#for {category, mix_tasks} <- @mix_tasks_by_category}
-
- {category}
-
- {#for mix_task <- mix_tasks}
-
-
-
-
-
- {mix_task.name}
-
-
- {/for}
- {/for}
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- Modules
-
-
-
- {#for {category, modules} <- @modules_by_category}
-
- {category}
-
- {#for module <- modules}
-
-
-
- {module.name}
-
-
- {/for}
- {/for}
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
"""
end
- defp render_dsls(assigns, dsls, path) do
+ def render_icon(assigns, "Guide") do
~F"""
-
- {#for dsl <- Enum.filter(dsls, &(&1.path == path))}
-
-
-
- {dsl.name}
-
-
- {render_dsls(assigns, dsls, path ++ [dsl.name])}
-
- {/for}
-
+
"""
end
def render_icon(assigns, "Resource") do
~F"""
-
+
"""
end
def render_icon(assigns, "Api") do
~F"""
-
+
"""
end
def render_icon(assigns, "DataLayer") do
~F"""
-
+
"""
end
def render_icon(assigns, "Flow") do
~F"""
-
+
"""
end
def render_icon(assigns, "Notifier") do
~F"""
-
+
"""
end
def render_icon(assigns, "Registry") do
~F"""
-
+
+ """
+ end
+
+ def render_icon(assigns, "Mix Task") do
+ ~F"""
+
"""
end
def render_icon(assigns, _) do
~F"""
-
+
"""
end
@@ -479,10 +419,10 @@ defmodule AshHqWeb.Components.DocSidebar do
)
end
- defp collapse(id, js \\ %JS{}) do
- js
- |> JS.toggle(to: "##{id}", time: 0)
- |> JS.toggle(to: "##{id}-chevron-down", time: 0)
- |> JS.toggle(to: "##{id}-chevron-right", time: 0)
+ def slug(string) do
+ string
+ |> String.downcase()
+ |> String.replace(" ", "_")
+ |> String.replace(~r/[^a-z0-9-_]/, "-")
end
end
diff --git a/lib/ash_hq_web/components/doc_sidebar_dsl_items.ex b/lib/ash_hq_web/components/doc_sidebar_dsl_items.ex
new file mode 100644
index 0000000..7a185b1
--- /dev/null
+++ b/lib/ash_hq_web/components/doc_sidebar_dsl_items.ex
@@ -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"""
+ 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])}
+ >
+
+
+ """
+ end
+end
diff --git a/lib/ash_hq_web/components/tree_view.ex b/lib/ash_hq_web/components/tree_view.ex
new file mode 100644
index 0000000..c6b940e
--- /dev/null
+++ b/lib/ash_hq_web/components/tree_view.ex
@@ -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"""
+
+
+ <#slot
+ :for={item <- @default}
+ context_put={Item, path: @id}
+ {item}
+ />
+
+
+ """
+ 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 `` element for the item."
+ prop class, :css_class, default: nil
+
+ @doc "Children `TreeView.Item` nodes."
+ slot default
+
+ def render(assigns) do
+ ~F"""
+
+
+
+
handle_click("#{@path}-#{@name}", @collapsable)}
+ class="flex flex-row items-center w-full"
+ >
+
+
+
+ {#if @icon}{@icon}{/if}
+ {@text}
+
+
+
+ <#slot context_put={Item, path: "#{@path}-#{@name}"} :for={item <- @default} {item} />
+
+
+ """
+ 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
diff --git a/mix.exs b/mix.exs
index e47e872..1b8db82 100644
--- a/mix.exs
+++ b/mix.exs
@@ -7,7 +7,7 @@ defmodule AshHq.MixProject do
version: "0.1.0",
elixir: "~> 1.12",
elixirc_paths: elixirc_paths(Mix.env()),
- compilers: Mix.compilers(),
+ compilers: Mix.compilers() ++ [:surface],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()