From 1265f289c0d6e95a12926bc82231a0831b96467a Mon Sep 17 00:00:00 2001 From: Mike Buhot Date: Mon, 14 Nov 2022 01:43:11 +1000 Subject: [PATCH] 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. --- .gitignore | 2 + assets/css/app.css | 3 + config/config.exs | 5 + config/dev.exs | 1 + lib/ash_hq_web/components/doc_sidebar.ex | 338 +++++++----------- .../components/doc_sidebar_dsl_items.ex | 46 +++ lib/ash_hq_web/components/tree_view.ex | 133 +++++++ mix.exs | 2 +- 8 files changed, 330 insertions(+), 200 deletions(-) create mode 100644 lib/ash_hq_web/components/doc_sidebar_dsl_items.ex create mode 100644 lib/ash_hq_web/components/tree_view.ex 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} /> -
- -
+ + + + + + + + + + + + + """ end - defp render_dsls(assigns, dsls, path) do + def render_icon(assigns, "Guide") do ~F""" - + """ 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""" + +
  • +
    + +
    +
      + <#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()