defmodule GitGudWeb.RepoLive.Badges do
@moduledoc """
The badge sheet: a repository's status badges in every style and
theme, with a contrast check and snippets to embed them.
"This repository now" shows every badge the repository has, grouped,
and doubles as a picker: switch on the README block, choose badges,
and one card holds the Markdown (or HTML) for all of them, in order,
ready to paste.
Its options (style, corners, scale, GG) are path segments, the same
ones the badge images take (`GitGud.Badges.Spec`), so any view of the
sheet can be linked: `/r/neiam/gitgud/badges/dot/2.5x/gg`.
"""
use GitGudWeb, :live_view
alias GitGud.Badges
alias GitGud.Badges.Palette
alias GitGud.Badges.Spec
alias GitGud.Badges.Svg
alias GitGud.Repositories
alias GitGudWeb.BadgeCatalog
alias Phoenix.LiveView.JS
@style_notes %{
"split" => "Solid blocks. Reads best in a row of badges at the top of a README.",
"dot" =>
"Quiet: the dot carries the status and the text stays in the page's own color. Suits docs and running text.",
"outline" => "A colored outline. Stands out on a busy page without a solid fill."
}
@coverage_yaml """
jobs:
test:
runs-on: docker
outputs:
gitgud-coverage: ${{ steps.coverage.outputs.percent }}
steps:
- uses: actions/checkout@v4
- run: mix test --cover
- id: coverage
run: echo "percent=87.3" >> "$GITHUB_OUTPUT"
audit:
runs-on: docker
outputs:
gitgud-conclusion: ${{ steps.scan.outputs.conclusion }}
gitgud-summary: ${{ steps.scan.outputs.summary }}
steps:
- uses: actions/checkout@v4
- id: scan
run: |
echo "conclusion=warning" >> "$GITHUB_OUTPUT"
echo "summary=1 no fix" >> "$GITHUB_OUTPUT"\
"""
@impl true
def mount(%{"owner" => owner, "name" => name}, _session, socket) do
repo =
Repositories.get_repository_by_path!(owner, name)
|> GitGud.Repo.preload([:owner, :organization])
{:ok,
socket
|> assign(:repo, repo)
|> assign(:handle, BadgeCatalog.handle(repo))
|> GitGudWeb.RepoLive.Header.assign_chrome(repo)
|> assign_items(repo)
|> assign(:snippet_params, %{"kind" => "ci", "theme" => "auto", "format" => "svg"})
|> assign(:readme_open, false)
|> assign(:readme_params, %{"theme" => "auto", "format" => "svg"})
|> assign(:page_title, "Badges — #{owner}/#{name}")}
end
@impl true
def handle_params(params, _uri, socket) do
case Spec.parse_options(Map.get(params, "options", [])) do
{:ok, spec} ->
{:noreply,
socket
|> assign(:spec, spec)
|> assign_snippets(socket.assigns.snippet_params)
|> assign_readme()}
:error ->
{:noreply, push_patch(socket, to: sheet_path(socket.assigns, %Spec{}))}
end
end
@impl true
def handle_event("snippet", %{"snippet" => params}, socket) do
{:noreply, assign_snippets(socket, params)}
end
def handle_event("toggle_readme", _params, socket) do
{:noreply, assign(socket, :readme_open, !socket.assigns.readme_open)}
end
def handle_event("toggle_item", %{"key" => key}, socket) do
selected = socket.assigns.selected
selected =
cond do
key in selected -> MapSet.delete(selected, key)
Enum.any?(socket.assigns.items, &(&1.key == key)) -> MapSet.put(selected, key)
true -> selected
end
{:noreply, socket |> assign(:selected, selected) |> assign_readme()}
end
def handle_event("readme_all", _params, socket) do
{:noreply,
socket
|> assign(:selected, MapSet.new(socket.assigns.items, & &1.key))
|> assign_readme()}
end
def handle_event("readme_none", _params, socket) do
{:noreply, socket |> assign(:selected, MapSet.new()) |> assign_readme()}
end
def handle_event("readme_options", %{"readme" => params}, socket) do
params = %{
"theme" => pick(params["theme"], Spec.themes(), "auto"),
"format" => pick(params["format"], Spec.formats(), "svg")
}
{:noreply, socket |> assign(:readme_params, params) |> assign_readme()}
end
# Every badge the repository has (`BadgeCatalog.items/1`), each with
# what it says right now.
defp assign_items(socket, repo) do
items = BadgeCatalog.items(repo)
keys = MapSet.new(items, & &1.key)
socket
|> assign(:items, items)
|> assign(:current, Map.new(items, &{&1.key, &1.content}))
|> assign(
:selected,
BadgeCatalog.readme_defaults() |> MapSet.new() |> MapSet.intersection(keys)
)
end
# The README card: the selected badges, in the order shown, as one line
# of Markdown and as HTML.
defp assign_readme(socket) do
%{items: items, selected: selected, readme_params: params} = socket.assigns
chosen = Enum.filter(items, &(&1.key in selected))
%{handle: handle, repo: repo, spec: spec} = socket.assigns
embeds =
Enum.map(chosen, fn item ->
BadgeCatalog.embed(
repo,
handle,
BadgeCatalog.spec(spec, item.key, params["theme"], params["format"])
)
end)
# The preview is the block's own images, so it shows exactly what the
# Markdown will: that theme, that format, `auto` following the
# viewer's system setting as it will for a README's reader.
previews = Enum.map(embeds, &%{path: &1.path, alt: &1.alt})
socket
|> assign(:readme_items, chosen)
|> assign(:readme_previews, previews)
|> assign(:readme_form, to_form(params, as: :readme))
|> assign(:readme_markdown, Enum.map_join(embeds, " ", & &1.markdown))
|> assign(:readme_html, Enum.map_join(embeds, "\n", & &1.html))
end
defp assign_snippets(socket, params) do
kind = pick(params["kind"], Enum.map(kind_options(socket.assigns), &elem(&1, 1)), "ci")
theme = pick(params["theme"], Spec.themes(), "auto")
format = pick(params["format"], Spec.formats(), "svg")
params = %{"kind" => kind, "theme" => theme, "format" => format}
%{repo: repo, handle: handle, spec: spec} = socket.assigns
badge = BadgeCatalog.spec(spec, kind, theme, format)
%{url: url, link: link, alt: alt} = e = BadgeCatalog.embed(repo, handle, badge)
snippets = [
%{id: "markdown", title: "Markdown", text: e.markdown},
%{id: "html", title: "HTML, separate dark and light images", text: e.html},
%{
id: "rst",
title: "reStructuredText",
text: ".. image:: #{url}\n :target: #{link}\n :alt: #{alt}"
},
%{id: "asciidoc", title: "AsciiDoc", text: "image:#{url}[#{alt},link=#{link}]"},
%{id: "url", title: "Image URL", text: url}
]
socket
|> assign(:snippet_params, params)
|> assign(:snippet_form, to_form(params, as: :snippet))
|> assign(:snippet_preview, e.path)
|> assign(:snippets, snippets)
end
defp pick(value, allowed, default), do: if(value in allowed, do: value, else: default)
defp item_path(%{handle: handle, repo: repo, spec: spec}, key),
do: BadgeCatalog.image_path(handle, repo, BadgeCatalog.spec(spec, key, "auto", "svg"))
# The snippet builder's badge choices: the plain kinds, then the
# repository's own workflows and packages by key.
defp kind_options(assigns) do
labels = BadgeCatalog.kind_labels()
for(kind <- Spec.kinds(), kind not in ~w(workflow package), do: {labels[kind], kind}) ++
for(
%{kind: "workflow", key: key, title: title} <- assigns.items,
do: {"Workflow: #{title}", key}
) ++
for %{kind: "package", key: key, title: title} <- assigns.items do
{_kind, _name, registry} = BadgeCatalog.split_key(key)
{"Package: #{title} (#{registry})", key}
end
end
# ── paths ─────────────────────────────────────────────────────────────
defp sheet_path(%{handle: handle, repo: repo}, spec) do
case Spec.option_segments(spec) do
[] -> ~p"/r/#{handle}/#{repo.name}/badges"
segments -> ~p"/r/#{handle}/#{repo.name}/badges/#{segments}"
end
end
defp sample_path(spec, kind, key, theme) do
~p"/badges/sample/#{kind}/#{key}/#{Spec.explicit_segments(%{spec | kind: kind, theme: theme})}"
end
# A fixed theme previews on its own background; `auto` on the page's.
defp readme_preview_style("auto"), do: nil
defp readme_preview_style(theme),
do:
"background:#{Palette.token_hex(theme, "base-100")};color:#{Palette.token_hex(theme, "base-content")}"
# `id`: a page may hold the same badge twice, and each needs its own
# clip path id.
defp inline_badge(content, spec, id),
do: Phoenix.HTML.raw(Svg.render(content, spec, :inline, id: "ggb-" <> id))
# ── template ──────────────────────────────────────────────────────────
@impl true
def render(assigns) do
assigns =
assigns
|> assign(:style_notes, @style_notes)
|> assign(:kind_labels, BadgeCatalog.kind_labels())
|> assign(:kinds, BadgeCatalog.kinds())
|> assign(:kind_options, kind_options(assigns))
|> assign(:coverage_yaml, @coverage_yaml)
|> assign(:groups, BadgeCatalog.groups())
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<style id="badge-theme-styles">
<%= Phoenix.HTML.raw(Palette.inline_stylesheet()) %>
</style>
<div class="space-y-10">
<GitGudWeb.RepoLive.Header.header
repo={@repo}
handle={@handle}
current_scope={@current_scope}
has_packages?={@has_packages?}
can_admin?={@can_admin?}
latest_run={@latest_run}
open_pulls={@open_pulls}
security_alerts={assigns[:security_alerts]}
/>
<header class="space-y-2">
<h1 class="text-xl font-semibold">Badges</h1>
<p class="opacity-70 max-w-prose">
Status badges for <span class="font-mono">{@repo.default_branch}</span>
to embed in a README or anywhere else that takes an image. Every option
below is part of the page's address, so you can link to any view of it.
</p>
</header>
<nav
id="badge-options"
aria-label="Badge options"
class="flex flex-wrap gap-x-8 gap-y-4 items-start rounded-box border border-base-300 bg-base-200/60 p-4"
>
<.option_group label="Style" id="badge-style">
<.option_link
:for={style <- Spec.styles()}
id={"badge-style-#{style}"}
patch={sheet_path(assigns, %{@spec | style: style})}
active={@spec.style == style}
>
{String.capitalize(style)}
</.option_link>
</.option_group>
<.option_group label="Corners" id="badge-corners">
<.option_link
:for={corners <- Spec.corners()}
id={"badge-corners-#{corners}"}
patch={sheet_path(assigns, %{@spec | corners: corners})}
active={@spec.corners == corners}
>
{if corners == "round", do: "Theme radius", else: "Square"}
</.option_link>
</.option_group>
<.option_group label="Scale" id="badge-scale">
<.option_link
:for={scale <- Spec.scales()}
id={"badge-scale-#{String.replace(scale, ".", "_")}"}
patch={sheet_path(assigns, %{@spec | scale: scale})}
active={@spec.scale == scale}
>
{String.replace(scale, "x", "×")}
</.option_link>
</.option_group>
<.option_group label="GG mark" id="badge-gg">
<.option_link
id="badge-gg-off"
patch={sheet_path(assigns, %{@spec | gg: false})}
active={!@spec.gg}
>
Off
</.option_link>
<.option_link
id="badge-gg-on"
patch={sheet_path(assigns, %{@spec | gg: true})}
active={@spec.gg}
>
On
</.option_link>
</.option_group>
<p id="badge-style-note" class="basis-full text-sm opacity-70">
{@style_notes[@spec.style]}
</p>
</nav>
<section id="badges-current" class="space-y-5">
<div class="flex flex-wrap items-end justify-between gap-3">
<div>
<h2 class="text-lg font-semibold">This repository now</h2>
<p class="text-sm opacity-70 max-w-prose">
Every badge this repository has, drawn in your current theme. GitGud pages show
badges this way, so they change with the reader's theme.
</p>
</div>
<button
type="button"
id="readme-toggle"
phx-click="toggle_readme"
aria-pressed={to_string(@readme_open)}
aria-controls="readme-card"
class={[
"inline-flex items-center gap-2 rounded-field border px-3 py-1.5 text-sm transition-colors duration-150",
if(@readme_open,
do: "bg-primary text-primary-content border-primary",
else: "border-base-300 hover:border-primary"
)
]}
>
<.icon name="hero-document-text" class="size-4" />
{if @readme_open, do: "Done choosing", else: "Build a README block"}
</button>
</div>
<p :if={@readme_open} id="readme-hint" class="text-sm opacity-70">
Select badges to add them to the block below. They appear in the order shown here.
</p>
<div :for={{group, group_label} <- @groups} class="space-y-2">
<% group_items = Enum.filter(@items, &(&1.group == group)) %>
<h3
:if={group_items != []}
class="font-mono text-[0.68rem] uppercase tracking-wider opacity-70"
>
{group_label}
</h3>
<ul
:if={group_items != []}
id={"badges-group-#{group}"}
class="grid gap-2 grid-cols-[repeat(auto-fill,minmax(min(100%,15rem),1fr))]"
>
<li :for={item <- group_items} id={"badge-item-#{item.index}"} data-badge={item.key}>
<.badge_tile
item={item}
spec={@spec}
href={item_path(assigns, item.key)}
choosing={@readme_open}
selected={item.key in @selected}
/>
</li>
</ul>
</div>
<p :if={@current["coverage"].value == "none"} class="text-sm opacity-70">
No coverage yet. See <a href="#badges-coverage" class="link">reporting to badges</a>
below.
</p>
<div
:if={@readme_open}
id="readme-card"
class="rounded-box border border-base-300 bg-base-200/60 p-4 space-y-4"
>
<div class="flex flex-wrap items-end justify-between gap-3">
<div>
<h3 class="font-semibold">README block</h3>
<p class="text-sm opacity-70">
{length(@readme_items)} {if length(@readme_items) == 1,
do: "badge",
else: "badges"}, with the style, corners, scale and GG setting above.
</p>
</div>
<div class="flex flex-wrap items-end gap-3">
<.form
for={@readme_form}
id="readme-form"
phx-change="readme_options"
class="flex gap-3 items-end"
>
<div class="w-36">
<.input
field={@readme_form[:theme]}
type="select"
label="Theme"
options={Spec.themes()}
/>
</div>
<div class="w-28">
<.input
field={@readme_form[:format]}
type="select"
label="Format"
options={[{"SVG", "svg"}, {"PNG", "png"}]}
/>
</div>
</.form>
<div class="flex gap-2 pb-3">
<button
type="button"
id="readme-all"
phx-click="readme_all"
class="btn btn-accent"
>
Select all
</button>
<button
type="button"
id="readme-none"
phx-click="readme_none"
class="btn btn-accent"
>
Clear
</button>
</div>
</div>
</div>
<%= if @readme_items == [] do %>
<p id="readme-empty" class="text-sm opacity-70">
Nothing selected yet. Select badges above to build the block.
</p>
<% else %>
<div
id="readme-preview"
class="flex flex-wrap gap-1.5 items-center rounded-box p-3 border border-base-300 transition-colors duration-200"
style={readme_preview_style(@readme_params["theme"])}
>
<img
:for={preview <- @readme_previews}
src={preview.path}
alt={preview.alt}
class="block max-w-full"
/>
</div>
<div id="readme-markdown" class="space-y-1.5">
<div class="flex items-center justify-between gap-2">
<span class="font-mono text-xs uppercase tracking-wider opacity-70">
Markdown
</span>
<button
type="button"
id="readme-copy-markdown"
class="font-mono text-xs px-2.5 py-1 rounded border border-base-300 bg-base-100 hover:border-primary transition-colors"
phx-click={
JS.dispatch("git_gud:copy", detail: %{text: @readme_markdown})
|> JS.transition("scale-95", time: 150)
}
>
Copy
</button>
</div>
<pre class="bg-base-100 rounded px-3 py-2 text-xs whitespace-pre-wrap break-all"><code>{@readme_markdown}</code></pre>
</div>
<details id="readme-html" class="group">
<summary class="cursor-pointer font-mono text-xs uppercase tracking-wider opacity-70 hover:opacity-100">
HTML, separate dark and light images
</summary>
<div class="mt-2 space-y-1.5">
<div class="flex justify-end">
<button
type="button"
id="readme-copy-html"
class="font-mono text-xs px-2.5 py-1 rounded border border-base-300 bg-base-100 hover:border-primary transition-colors"
phx-click={JS.dispatch("git_gud:copy", detail: %{text: @readme_html})}
>
Copy
</button>
</div>
<pre class="bg-base-100 rounded px-3 py-2 text-xs overflow-x-auto"><code>{@readme_html}</code></pre>
</div>
</details>
<% end %>
</div>
</section>
<section id="badges-themes" class="space-y-3">
<div>
<h2 class="text-lg font-semibold">Every theme</h2>
<p class="text-sm opacity-70">
Fixed to one theme, for embedding where you know the background. Each card sits on
its theme's own background. Select a badge to open its image.
</p>
</div>
<div class="grid gap-4 grid-cols-[repeat(auto-fill,minmax(min(100%,22rem),1fr))]">
<article
:for={theme <- Palette.themes()}
id={"badge-theme-#{theme}"}
class="rounded-box p-4 space-y-3 min-w-0 border border-black/10"
style={"background:#{Palette.token_hex(theme, "base-100")};color:#{Palette.token_hex(theme, "base-content")}"}
>
<div class="flex items-baseline justify-between gap-2">
<h3 class="font-mono font-semibold">{theme}</h3>
<span class="font-mono text-xs opacity-70">
{Palette.scheme(theme)} · {if Palette.radius_px(theme) >= 10,
do: "pill",
else: "#{trunc(Palette.radius_px(theme))}px"}
</span>
</div>
<div
:for={kind <- @kinds ++ ["workflow", "package", "mark"]}
class="grid grid-cols-[5.5rem_1fr] gap-2 items-start"
>
<span class="font-mono text-[0.68rem] uppercase tracking-wider opacity-70 pt-1">
{@kind_labels[kind]}
</span>
<div class="flex flex-wrap gap-1.5 items-center min-w-0">
<a
:for={key <- Badges.sample_keys(kind)}
href={sample_path(@spec, kind, key, theme)}
target="_blank"
rel="noopener"
class="transition-transform duration-150 hover:-translate-y-0.5"
>
<img
src={sample_path(@spec, kind, key, theme)}
alt={"#{@kind_labels[kind]} example: #{key}"}
loading="lazy"
class="block max-w-full"
/>
</a>
</div>
</div>
</article>
</div>
</section>
<section id="badges-contrast" class="space-y-3">
<div>
<h2 class="text-lg font-semibold">Contrast, {@spec.style} style</h2>
<p class="text-sm opacity-70 max-w-prose">
Badge text is 11px, so it needs 4.5:1. The status dot is a shape and needs 3:1.
Where a theme's usual text color falls short, badges use another, named under the ratio.
</p>
</div>
<div class="overflow-x-auto rounded-box border border-base-300">
<table id="badge-contrast-table" class="w-full text-sm tabular-nums">
<thead class="bg-base-200 text-xs uppercase tracking-wider font-mono">
<tr>
<th class="px-3 py-2 text-left font-normal opacity-70">Theme</th>
<th
:for={{name, _fg, _bg, _min} <- Palette.audit_pairs(@spec.style)}
class="px-3 py-2 text-left font-normal opacity-70 whitespace-nowrap"
>
{name}
</th>
</tr>
</thead>
<tbody>
<tr :for={theme <- Palette.themes()} class="border-t border-base-300">
<td class="px-3 py-2 font-mono">{theme}</td>
<td
:for={{name, fg, bg, min} <- Palette.audit_pairs(@spec.style)}
class="px-3 py-2 whitespace-nowrap"
>
<% ratio = Palette.contrast(theme, fg, bg) %>
<span class="inline-flex items-center gap-2">
<span
class="font-mono text-xs px-1.5 py-0.5 rounded"
style={"background:#{Palette.hex(theme, bg)};color:#{Palette.hex(theme, fg)}"}
>
{if String.ends_with?(name, "dot"), do: "●", else: "Aa"}
</span>
<span class="leading-tight">
<span class={[
"font-mono",
if(ratio >= min, do: "text-success", else: "text-error font-bold")
]}>
{:erlang.float_to_binary(ratio, decimals: 2)}
</span>
<span class="block font-mono text-[0.65rem] opacity-60">
{Palette.token(theme, fg)}
</span>
</span>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="badges-embed" class="space-y-4">
<div>
<h2 class="text-lg font-semibold">Embed</h2>
<p class="text-sm opacity-70 max-w-prose">
Uses the style, corners, scale and GG setting above. <span class="font-mono">auto</span>
follows the reader's light or dark setting on any site that shows SVG. A PNG can't, so
the HTML snippet offers a dark and a light image instead.
</p>
</div>
<.form
for={@snippet_form}
id="badge-snippet-form"
phx-change="snippet"
class="flex flex-wrap gap-4 items-end"
>
<div class="w-40">
<.input
field={@snippet_form[:kind]}
type="select"
label="Badge"
options={@kind_options}
/>
</div>
<div class="w-40">
<.input
field={@snippet_form[:theme]}
type="select"
label="Theme"
options={Spec.themes()}
/>
</div>
<div class="w-32">
<.input
field={@snippet_form[:format]}
type="select"
label="Format"
options={[{"SVG", "svg"}, {"PNG", "png"}]}
/>
</div>
</.form>
<div id="badge-snippet-preview" class="min-h-12 flex items-center">
<img src={@snippet_preview} alt="Preview of the selected badge" />
</div>
<div class="space-y-3">
<div :for={snippet <- @snippets} id={"badge-snippet-#{snippet.id}"} class="space-y-1.5">
<div class="flex items-center justify-between gap-2">
<span class="font-mono text-xs uppercase tracking-wider opacity-70">
{snippet.title}
</span>
<button
type="button"
id={"badge-copy-#{snippet.id}"}
class="font-mono text-xs px-2.5 py-1 rounded border border-base-300 hover:border-primary transition-colors"
phx-click={JS.dispatch("git_gud:copy", detail: %{text: snippet.text})}
>
Copy
</button>
</div>
<pre class="bg-base-200 rounded px-3 py-2 text-xs overflow-x-auto"><code>{snippet.text}</code></pre>
</div>
</div>
</section>
<section id="badges-coverage" class="space-y-3">
<div>
<h2 class="text-lg font-semibold">Reporting to badges</h2>
<p class="text-sm opacity-70 max-w-prose">
Two job outputs feed the badges. <span class="font-mono">gitgud-coverage</span>
takes a percentage: when the job succeeds, the commit gets a
<span class="font-mono">coverage</span>
status and the coverage badge shows the newest one on {@repo.default_branch}.
<span class="font-mono">gitgud-summary</span>
says in a few words what a job found, like <span class="font-mono">1 no fix</span>
from an audit: the workflow's badge shows it in place of
<span class="font-mono">warning</span>
or <span class="font-mono">failing</span>, and so does the job's check on the commit.
</p>
</div>
<pre
id="badge-coverage-example"
class="bg-base-200 rounded px-3 py-2 text-xs overflow-x-auto"
><code>{@coverage_yaml}</code></pre>
</section>
</div>
</Layouts.app>
"""
end
attr :item, :map, required: true
attr :spec, :map, required: true
attr :href, :string, required: true
attr :choosing, :boolean, default: false
attr :selected, :boolean, default: false
# One badge with what it is. Choosing for the README block, it toggles
# the badge in or out; otherwise it opens the badge's image.
defp badge_tile(assigns) do
~H"""
<%= if @choosing do %>
<button
type="button"
phx-click="toggle_item"
phx-value-key={@item.key}
aria-pressed={to_string(@selected)}
class={[
"w-full h-full text-left rounded-box border p-3 flex items-start gap-3 transition-all duration-150",
if(@selected,
do: "border-primary bg-primary/10 shadow-sm",
else: "border-base-300 hover:border-primary/60 opacity-80 hover:opacity-100"
)
]}
>
<span class={[
"mt-0.5 size-4 shrink-0 rounded border flex items-center justify-center transition-colors",
if(@selected,
do: "bg-primary border-primary text-primary-content",
else: "border-base-content/40"
)
]}>
<.icon :if={@selected} name="hero-check" class="size-3" />
</span>
<.tile_body item={@item} spec={@spec} />
</button>
<% else %>
<a
href={@href}
target="_blank"
rel="noopener"
class="w-full h-full rounded-box border border-base-300 p-3 flex items-start gap-3 transition-all duration-150 hover:border-primary/60 hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-primary"
>
<.tile_body item={@item} spec={@spec} />
</a>
<% end %>
"""
end
attr :item, :map, required: true
attr :spec, :map, required: true
defp tile_body(assigns) do
~H"""
<span class="min-w-0 space-y-1.5">
<span class="block max-w-full overflow-x-auto">
{inline_badge(@item.content, @spec, "item-#{@item.index}")}
</span>
<span class="block text-sm font-medium leading-tight truncate">{@item.title}</span>
<span class="block text-xs opacity-60 leading-tight">{@item.hint}</span>
</span>
"""
end
attr :label, :string, required: true
attr :id, :string, required: true
slot :inner_block, required: true
defp option_group(assigns) do
~H"""
<div id={@id} class="space-y-1.5">
<span class="block font-mono text-[0.68rem] uppercase tracking-wider opacity-70">
{@label}
</span>
<div class="inline-flex rounded-field border border-base-300 overflow-hidden bg-base-100">
{render_slot(@inner_block)}
</div>
</div>
"""
end
attr :id, :string, required: true
attr :patch, :string, required: true
attr :active, :boolean, default: false
slot :inner_block, required: true
defp option_link(assigns) do
~H"""
<.link
id={@id}
patch={@patch}
aria-current={@active && "page"}
class={[
"px-3 py-1.5 text-sm transition-colors duration-150 [&:not(:first-child)]:border-l border-base-300",
if(@active,
do: "bg-primary text-primary-content",
else: "hover:bg-base-200"
)
]}
>
{render_slot(@inner_block)}
</.link>
"""
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
31.2 KiB · text
History
b868907