diff --git a/test/website/command_palette_test.exs b/test/website/command_palette_test.exs new file mode 100644 index 0000000..627ac5f --- /dev/null +++ b/test/website/command_palette_test.exs @@ -0,0 +1,36 @@ +defmodule Shadix.Website.CommandPaletteTest do + use ExUnit.Case, async: true + + alias Shadix.Website.CommandPalette + + describe "entries/0" do + test "returns the Introduction page plus every catalog component, pages first" do + entries = CommandPalette.entries() + + pages = Enum.filter(entries, &(&1.type == :page)) + components = Enum.filter(entries, &(&1.type == :component)) + + # One page for now: Introduction -> "/" + assert length(pages) == 1 + [intro] = pages + assert intro.label == "Introduction" + assert intro.path == "/" + assert intro.sub == "Getting started" + + # Every documented component is present, labelled by its humanized slug, + # pointing at /components/, with its category as the sub-label. + slugs = Enum.map(components, & &1.slug) |> Enum.sort() + catalog_slugs = Enum.map(Shadix.Website.Components.Catalog.all(), & &1.slug) |> Enum.sort() + assert slugs == catalog_slugs + + button = Enum.find(components, &(&1.slug == "button")) + assert button.label == "Button" + assert button.path == "/components/button" + assert button.sub == "Forms" + + # Pages come before components in the flat list. + assert Enum.find_index(entries, &(&1.type == :page)) < + Enum.find_index(entries, &(&1.type == :component)) + end + end +end diff --git a/website/command_palette.ex b/website/command_palette.ex new file mode 100644 index 0000000..ce52560 --- /dev/null +++ b/website/command_palette.ex @@ -0,0 +1,49 @@ +defmodule Shadix.Website.CommandPalette do + @moduledoc """ + A ⌘K / Ctrl+K command palette for the Shadix docs site. + + A LiveComponent that renders a search trigger (in the top nav) plus a modal + dialog built on `Shadix.Components.Dialog`, hosting a `Shadix.Components.Command` + whose items are the site's docs pages and documented components. Typing filters + server-side (per keystroke); results are ranked by relevance and shown in two + labelled sections — Pages and Components — the higher-scoring section first. + Selecting an item live-navigates to its page and closes the palette. + + The ranking/sectioning helpers (`entries/0`, `score/2`, `search/1`) are pure so + they can be unit-tested without a live process. + """ + + alias Shadix.Website.Components.Catalog + + # The non-component docs pages. Today: just Introduction. Add entries here to + # populate the Pages section further (each needs a route + a doc module). + @pages [ + %{type: :page, label: "Introduction", path: "/", sub: "Getting started", slug: nil} + ] + + @doc """ + The flat list of searchable entries: pages first, then every documented + component from `Catalog.all()` mapped to a labelled entry. + """ + def entries do + @pages ++ component_entries() + end + + defp component_entries do + Enum.map(Catalog.all(), fn entry -> + %{ + type: :component, + label: humanize(entry.slug), + path: "/components/#{entry.slug}", + sub: entry.category, + slug: entry.slug + } + end) + end + + defp humanize(slug) do + slug + |> String.split("_") + |> Enum.map_join(" ", &String.capitalize/1) + end +end