fix: better styling for docs
This commit is contained in:
@@ -20,6 +20,7 @@ This document provides context and best practices for GitHub Copilot when workin
|
||||
- you must use ecto to generate migrations and snapshots
|
||||
- on MacOS we use native menus and you have to hook them into the intercept for new menu items
|
||||
- there are two areas of localization, you sometimes need both (menus for example)
|
||||
- localization is done with elixier gettext and you need mix gettext.extract to update translation files
|
||||
- all automatic AI activities must be gated by airplane (offline) mode of the app and either use the local model or inform the user via toast
|
||||
- metadata needs to be flushed to the filesystem and needs to be included in metadata diff tool and in rebuild from filesystem. All three aspects have to be in sync with each other.
|
||||
- if you add new metadata, add them to publishing, metadata-diff and rebuild-from-database
|
||||
|
||||
@@ -26,14 +26,26 @@
|
||||
<div class="misc-editor-content">
|
||||
<%= case @misc_editor.kind do %>
|
||||
<% :documentation -> %>
|
||||
<article class="misc-card help-doc-markdown" data-testid="help-documentation">
|
||||
<%= markdown_html(@misc_editor.markdown) %>
|
||||
</article>
|
||||
<div class="documentation-view">
|
||||
<main class="documentation-scroll">
|
||||
<div class="documentation-content markdown-body">
|
||||
<article class="documentation-article help-doc-markdown" data-testid="help-documentation">
|
||||
<%= markdown_html(@misc_editor.markdown) %>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<% :api_documentation -> %>
|
||||
<article class="misc-card help-doc-markdown" data-testid="help-api-documentation">
|
||||
<%= markdown_html(@misc_editor.markdown) %>
|
||||
</article>
|
||||
<div class="documentation-view">
|
||||
<main class="documentation-scroll">
|
||||
<div class="documentation-content markdown-body">
|
||||
<article class="documentation-article help-doc-markdown" data-testid="help-api-documentation">
|
||||
<%= markdown_html(@misc_editor.markdown) %>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<% :site_validation -> %>
|
||||
<div class="misc-columns">
|
||||
|
||||
@@ -1236,21 +1236,25 @@ defmodule BDS.Scripting.ApiDocs do
|
||||
end
|
||||
|
||||
defp table_of_contents do
|
||||
@methods
|
||||
|> Enum.map(& &1.module)
|
||||
|> Enum.uniq()
|
||||
module_names()
|
||||
|> Enum.map(fn module_name -> "- [#{module_name}](##{module_name})" end)
|
||||
|> Kernel.++(["- [Data Structures](#data-structures)"])
|
||||
end
|
||||
|
||||
defp render_modules do
|
||||
@methods
|
||||
|> Enum.group_by(& &1.module)
|
||||
|> Enum.flat_map(fn {module_name, methods} ->
|
||||
module_names()
|
||||
|> Enum.flat_map(fn module_name ->
|
||||
methods = module_methods(module_name)
|
||||
|
||||
[
|
||||
"## #{module_name}",
|
||||
"",
|
||||
"**Module APIs**",
|
||||
"",
|
||||
Enum.map(methods, fn method -> "- [#{method.module}.#{method.name}](##{method.module}#{method.name})" end),
|
||||
"",
|
||||
Enum.map(methods, &render_method/1),
|
||||
"[↑ Back to Table of contents](#table-of-contents)",
|
||||
""
|
||||
]
|
||||
end)
|
||||
@@ -1269,6 +1273,14 @@ defmodule BDS.Scripting.ApiDocs do
|
||||
"**Response specification**",
|
||||
"",
|
||||
"- Return type: `#{method.returns}`",
|
||||
render_nullability(method.returns),
|
||||
render_data_structure_references(method.returns),
|
||||
"",
|
||||
"**Example response**",
|
||||
"",
|
||||
"```lua",
|
||||
render_example_response(method.returns),
|
||||
"```",
|
||||
"",
|
||||
"**Example call**",
|
||||
"",
|
||||
@@ -1277,6 +1289,7 @@ defmodule BDS.Scripting.ApiDocs do
|
||||
"```",
|
||||
""
|
||||
]
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp render_params([]), do: ["- None"]
|
||||
@@ -1289,18 +1302,191 @@ defmodule BDS.Scripting.ApiDocs do
|
||||
end
|
||||
|
||||
defp example_call(method) do
|
||||
args =
|
||||
method.params
|
||||
|> Enum.map(fn param -> example_value(param.type) end)
|
||||
|> Enum.join(", ")
|
||||
args = Enum.map_join(method.params, ", ", &example_argument/1)
|
||||
|
||||
"local result = bds.#{method.module}.#{method.name}(#{args})"
|
||||
end
|
||||
|
||||
defp example_value("string"), do: "\"value\""
|
||||
defp example_value("table"), do: "{}"
|
||||
defp example_value("integer"), do: "1"
|
||||
defp example_value(_type), do: "nil"
|
||||
defp module_names do
|
||||
@methods
|
||||
|> Enum.map(& &1.module)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp module_methods(module_name) do
|
||||
Enum.filter(@methods, &(&1.module == module_name))
|
||||
end
|
||||
|
||||
defp render_nullability(returns) do
|
||||
if nullable_return?(returns) do
|
||||
"- Nullability: Returns `nil` when no matching value exists or the operation cannot produce a value."
|
||||
end
|
||||
end
|
||||
|
||||
defp render_data_structure_references(returns) do
|
||||
case response_structure_names(returns) do
|
||||
[] -> nil
|
||||
names -> "- Data structures: `#{Enum.join(names, "`, `")}`"
|
||||
end
|
||||
end
|
||||
|
||||
defp render_example_response(returns) do
|
||||
returns
|
||||
|> example_response_value()
|
||||
|> render_lua_value(0)
|
||||
end
|
||||
|
||||
defp example_argument(%{name: name, type: type}) do
|
||||
example_argument_value(name, type)
|
||||
end
|
||||
|
||||
defp example_argument_value(name, "string") do
|
||||
case name do
|
||||
"id" -> "\"id-1\""
|
||||
suffix when suffix in ["post_id", "media_id", "project_id", "tag_id", "target_tag_id"] -> "\"id-1\""
|
||||
"source_tag_ids" -> "{\"id-1\", \"id-2\"}"
|
||||
"language" -> "\"en\""
|
||||
"status" -> "\"draft\""
|
||||
"kind" -> "\"post\""
|
||||
"slug" -> "\"example-slug\""
|
||||
"title" -> "\"Example Title\""
|
||||
"name" -> "\"Example Name\""
|
||||
"query" -> "\"example query\""
|
||||
"content" -> "\"Example content\""
|
||||
"message" -> "\"Update content\""
|
||||
"folder_path" -> "\"/Users/me/Sites/example\""
|
||||
"source_path" -> "\"/Users/me/Pictures/example.jpg\""
|
||||
"item_path" -> "\"/Users/me/Sites/example/output/index.html\""
|
||||
"action" -> "\"save\""
|
||||
_ -> "\"value\""
|
||||
end
|
||||
end
|
||||
|
||||
defp example_argument_value("limit", "integer"), do: "10"
|
||||
defp example_argument_value(_name, "integer"), do: "1"
|
||||
defp example_argument_value(_name, "number"), do: "1.0"
|
||||
|
||||
defp example_argument_value(name, "table") do
|
||||
case name do
|
||||
"data" -> "{title = \"Example Title\"}"
|
||||
"filters" -> "{status = \"draft\"}"
|
||||
"options" -> "{}"
|
||||
"updates" -> "{name = \"Updated Blog\"}"
|
||||
"prefs" -> "{provider = \"filesystem\"}"
|
||||
"credentials" -> "{provider = \"sftp\"}"
|
||||
"target_ids" -> "{\"id-2\", \"id-3\"}"
|
||||
"exclude_tags" -> "{\"draft\"}"
|
||||
_ -> "{}"
|
||||
end
|
||||
end
|
||||
|
||||
defp example_argument_value(_name, _type), do: "nil"
|
||||
|
||||
defp nullable_return?(returns), do: String.contains?(returns, "nil")
|
||||
|
||||
defp response_structure_names(returns) do
|
||||
structure_names = MapSet.new(Enum.map(@data_structures, & &1.name))
|
||||
|
||||
returns
|
||||
|> String.split(~r/\s*\|\s*/)
|
||||
|> Enum.map(&String.replace(&1, "[]", ""))
|
||||
|> Enum.reject(&(&1 in ["nil", "boolean", "string", "integer", "number", "table"]))
|
||||
|> Enum.filter(&MapSet.member?(structure_names, &1))
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp example_response_value(returns) do
|
||||
cond do
|
||||
returns == "nil" -> nil
|
||||
nullable_return?(returns) -> {:nullable, example_response_value(non_nil_return(returns))}
|
||||
String.ends_with?(returns, "[]") -> [example_value_for_type(String.trim_trailing(returns, "[]"))]
|
||||
true -> example_value_for_type(returns)
|
||||
end
|
||||
end
|
||||
|
||||
defp non_nil_return(returns) do
|
||||
returns
|
||||
|> String.split(~r/\s*\|\s*/)
|
||||
|> Enum.reject(&(&1 == "nil"))
|
||||
|> List.first()
|
||||
end
|
||||
|
||||
defp example_value_for_type("boolean"), do: true
|
||||
defp example_value_for_type("string"), do: "value"
|
||||
defp example_value_for_type("integer"), do: 1
|
||||
defp example_value_for_type("number"), do: 1.0
|
||||
defp example_value_for_type("nil"), do: nil
|
||||
defp example_value_for_type("table"), do: [{"key", "value"}]
|
||||
|
||||
defp example_value_for_type(type) do
|
||||
case Enum.find(@data_structures, &(&1.name == type)) do
|
||||
nil -> [{"key", "value"}]
|
||||
structure -> Enum.map(structure.fields, fn field -> {field.name, example_field_value(field.type)} end)
|
||||
end
|
||||
end
|
||||
|
||||
defp example_field_value(type) do
|
||||
cond do
|
||||
String.contains?(type, " | nil") -> nil
|
||||
String.ends_with?(type, "[]") -> [example_value_for_type(String.trim_trailing(type, "[]"))]
|
||||
true -> example_value_for_type(type)
|
||||
end
|
||||
end
|
||||
|
||||
defp render_lua_value({:nullable, value}, indent) do
|
||||
["nil -- or", render_lua_value(value, indent)]
|
||||
|> Enum.join("\n")
|
||||
end
|
||||
|
||||
defp render_lua_value(true, _indent), do: "true"
|
||||
defp render_lua_value(false, _indent), do: "false"
|
||||
defp render_lua_value(nil, _indent), do: "nil"
|
||||
defp render_lua_value(value, _indent) when is_integer(value), do: Integer.to_string(value)
|
||||
defp render_lua_value(value, _indent) when is_float(value), do: :erlang.float_to_binary(value, [:compact])
|
||||
defp render_lua_value(value, _indent) when is_binary(value), do: inspect(value)
|
||||
|
||||
defp render_lua_value([], _indent), do: "{}"
|
||||
|
||||
defp render_lua_value(list, indent) when is_list(list) do
|
||||
if keyword_like_list?(list) do
|
||||
render_lua_table(list, indent)
|
||||
else
|
||||
render_lua_array(list, indent)
|
||||
end
|
||||
end
|
||||
|
||||
defp keyword_like_list?(list) do
|
||||
Enum.all?(list, fn
|
||||
{key, _value} when is_binary(key) -> true
|
||||
_ -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp render_lua_table(entries, indent) do
|
||||
outer_indent = indent_spaces(indent)
|
||||
inner_indent = indent_spaces(indent + 2)
|
||||
|
||||
rendered_entries =
|
||||
Enum.map_join(entries, ",\n", fn {key, value} ->
|
||||
"#{inner_indent}#{key} = #{render_lua_value(value, indent + 2)}"
|
||||
end)
|
||||
|
||||
"{\n#{rendered_entries}\n#{outer_indent}}"
|
||||
end
|
||||
|
||||
defp render_lua_array(values, indent) do
|
||||
outer_indent = indent_spaces(indent)
|
||||
inner_indent = indent_spaces(indent + 2)
|
||||
|
||||
rendered_values =
|
||||
Enum.map_join(values, ",\n", fn value ->
|
||||
"#{inner_indent}#{render_lua_value(value, indent + 2)}"
|
||||
end)
|
||||
|
||||
"{\n#{rendered_values}\n#{outer_indent}}"
|
||||
end
|
||||
|
||||
defp indent_spaces(indent), do: String.duplicate(" ", indent)
|
||||
|
||||
defp render_data_structures do
|
||||
Enum.flat_map(@data_structures, fn structure ->
|
||||
|
||||
183
priv/ui/app.css
183
priv/ui/app.css
@@ -4457,6 +4457,189 @@ button svg * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.help-doc-view {
|
||||
--doc-bg: var(--panel-1, #1e1e1e);
|
||||
--doc-surface: var(--panel-2, #252526);
|
||||
--doc-border: var(--line, #3c3c3c);
|
||||
--doc-text: var(--vscode-editor-foreground, #d4d4d4);
|
||||
--doc-muted: var(--vscode-descriptionForeground, #9da3ad);
|
||||
--doc-link: var(--vscode-textLink-foreground, #9cdcfe);
|
||||
--doc-code-bg: var(--vscode-textCodeBlock-background, rgba(0, 0, 0, 0.2));
|
||||
--doc-hover: var(--vscode-list-hoverBackground, rgba(255, 255, 255, 0.06));
|
||||
}
|
||||
|
||||
.help-doc-view .misc-editor-content {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.documentation-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
background: var(--doc-bg);
|
||||
}
|
||||
|
||||
.documentation-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 28px 24px 40px;
|
||||
background: var(--doc-bg);
|
||||
}
|
||||
|
||||
.documentation-content {
|
||||
max-width: 920px;
|
||||
margin: 0 auto;
|
||||
color: var(--doc-text);
|
||||
}
|
||||
|
||||
.documentation-article {
|
||||
background: var(--doc-surface);
|
||||
padding: 18px 20px 24px;
|
||||
border: 1px solid var(--doc-border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body > .documentation-article > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body > .documentation-article > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body h1,
|
||||
.documentation-content.markdown-body h2,
|
||||
.documentation-content.markdown-body h3 {
|
||||
color: var(--doc-text);
|
||||
border-bottom: 1px solid var(--doc-border);
|
||||
padding-bottom: 6px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body h1 {
|
||||
font-size: 1.9rem;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body h2 {
|
||||
margin-top: 2rem;
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body h3 {
|
||||
margin-top: 1.6rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body p,
|
||||
.documentation-content.markdown-body li,
|
||||
.documentation-content.markdown-body td,
|
||||
.documentation-content.markdown-body th {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body a {
|
||||
color: var(--doc-link);
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 0.14em;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body a:hover {
|
||||
color: var(--doc-text);
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body hr {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--doc-border);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body code {
|
||||
background: var(--doc-code-bg);
|
||||
padding: 0.12em 0.4em;
|
||||
border-radius: 4px;
|
||||
font: 0.92em/1.45 "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body pre {
|
||||
margin: 0.9rem 0 1.2rem;
|
||||
background: var(--doc-code-bg);
|
||||
border: 1px solid var(--doc-border);
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body pre code {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body blockquote {
|
||||
margin: 1rem 0;
|
||||
padding: 0 0 0 12px;
|
||||
border-left: 3px solid var(--doc-border);
|
||||
color: var(--doc-muted);
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body table {
|
||||
width: 100%;
|
||||
margin: 1rem 0 1.4rem;
|
||||
border-collapse: collapse;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body th,
|
||||
.documentation-content.markdown-body td {
|
||||
border: 1px solid var(--doc-border);
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body th {
|
||||
background: var(--doc-hover);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body ul,
|
||||
.documentation-content.markdown-body ol {
|
||||
margin: 0.85rem 0 1rem;
|
||||
padding-left: 1.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body li {
|
||||
margin: 0.3rem 0;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body li > ul,
|
||||
.documentation-content.markdown-body li > ol {
|
||||
margin-top: 0.35rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body strong {
|
||||
color: var(--doc-text);
|
||||
}
|
||||
|
||||
.documentation-content.markdown-body img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.misc-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
|
||||
@@ -1047,6 +1047,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
|
||||
assert has_element?(view, ".tab[data-tab-type='documentation'] .tab-title", "Documentation")
|
||||
assert has_element?(view, "[data-testid='help-documentation']")
|
||||
assert has_element?(view, ".documentation-content.markdown-body .documentation-article")
|
||||
assert render(view) =~ "bDS2 User Guide"
|
||||
end
|
||||
|
||||
@@ -1067,6 +1068,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
)
|
||||
|
||||
assert has_element?(view, "[data-testid='help-api-documentation']")
|
||||
assert has_element?(view, ".documentation-content.markdown-body .documentation-article")
|
||||
assert render(view) =~ "API Documentation"
|
||||
assert render(view) =~ "local result = bds.posts.get"
|
||||
end
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
defmodule BDS.Scripting.ApiDocumentationTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "rendered API docs include richer module indexes and example responses" do
|
||||
rendered = BDS.Scripting.ApiDocs.render()
|
||||
|
||||
assert rendered =~ "**Module APIs**"
|
||||
assert rendered =~ "- [projects.create](#projectscreate)"
|
||||
assert rendered =~ "**Example response**"
|
||||
assert rendered =~ "- Data structures: `ProjectData`"
|
||||
assert rendered =~ "[↑ Back to Table of contents](#table-of-contents)"
|
||||
end
|
||||
|
||||
test "API.md matches the generated Lua scripting contract" do
|
||||
api_doc_path = Path.expand("../../../API.md", __DIR__)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user