Skip to content

Commit 52fa316

Browse files
authored
feat(landing): optional hero logo for DocsUI::Landing (c.landing.logo) (#57)
DocsUI::Landing's hero was eyebrow + title + lead, with no way to show a product's own brand mark — the first thing a real consumer hit when adopting it on a mounted docs app. Add an optional hero logo above the eyebrow, in two forms: - inline single-path SVG: { svg: "<path d>", viewbox:, label: } — rendered with fill: currentColor so a single-color mark adapts to the theme (light/dark); - image: { src:, alt: } — resolved through the site's asset pipeline (image_url). nil (the default) omits it, so it's fully backwards-compatible. Normalized into a DocsKit::LandingConfig::Logo value object (like Cta/Feature). Dogfooded on the docs-kit site (an inline stacked-docs glyph) with a system-spec assertion; the generator initializer + component docs document it. Full gem suite 759 green, 94.73% line coverage, rubocop clean; dogfood system + request specs green.
1 parent c9c3bef commit 52fa316

8 files changed

Lines changed: 122 additions & 7 deletions

File tree

.rubocop.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ Metrics/ClassLength:
120120
- "lib/docs_kit/configuration.rb"
121121
- "lib/docs_kit/open_api/operation.rb"
122122
- "lib/docs_kit/open_api/schema.rb"
123+
# A composite renderer: logo + hero (eyebrow/title/lead/install/ctas) + a
124+
# feature grid + a doc index. Each part is a small focused method; the class
125+
# is long only because it renders several sections, like the OpenAPI ones above.
126+
- "app/components/docs_ui/landing.rb"
123127

124128
Metrics/MethodLength:
125129
Max: 25

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818

1919
### Added
2020

21+
- **`DocsUI::Landing` hero logo (`c.landing.logo`).** The landing hero now takes
22+
an optional brand mark above the eyebrow, in two forms: an inline single-path
23+
SVG (`{ svg: "<path d>", viewbox:, label: }`, rendered with `fill: currentColor`
24+
so it adapts to the theme) or an image (`{ src:, alt: }`, resolved through the
25+
site's asset pipeline). nil (the default) omits it, so it's backwards-compatible.
26+
This closes the gap a mounted docs app hit — a hero with no way to show the
27+
product's own logo — the first consumer to try `DocsUI::Landing` on a real site.
2128
- **`DocsUI::Landing` — a config-driven marketing landing page.** Every consuming
2229
site (and this dogfood site) was hand-rolling a home page; now render
2330
`DocsUI::Landing` and drive it from a new `c.landing` config block

app/components/docs_ui/landing.rb

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# frozen_string_literal: true
22

33
module DocsUI
4-
# The marketing landing page — a hero (eyebrow + title + lead + optional install
5-
# snippet + CTA buttons), a feature-card grid, and a registry-grouped
6-
# documentation index — rendered inside DocsUI::Shell. Every consuming site was
7-
# hand-rolling this; drive it from config instead:
4+
# The marketing landing page — a hero (an optional brand logo + eyebrow + title +
5+
# lead + optional install snippet + CTA buttons), a feature-card grid, and a
6+
# registry-grouped documentation index — rendered inside DocsUI::Shell. Every
7+
# consuming site was hand-rolling this; drive it from config instead:
88
#
99
# # config/initializers/docs_kit.rb
1010
# DocsKit.configure do |c|
11+
# c.landing.logo = { svg: "M4 2h9l5 5…Z", viewbox: "0 0 22 24", label: "Acme" }
1112
# c.landing.eyebrow = "Developer Docs"
1213
# c.landing.title = "Jobs & events on **Postgres**" # ** ** → primary color
1314
# c.landing.lead = "PostgreSQL-native jobs + event bus for Rails."
@@ -30,6 +31,9 @@ module DocsUI
3031
# walks the same #docs-content region Shell stamps.
3132
class Landing < Phlex::HTML
3233
include Phlex::Rails::Helpers::Request
34+
# For the image-form hero logo (c.landing.logo = { src: … }): resolve the asset
35+
# path through the site's pipeline to its digested /assets URL.
36+
include Phlex::Rails::Helpers::ImageURL
3337

3438
def view_template
3539
render DocsUI::Shell.new(title: landing.eyebrow || config.brand) do
@@ -50,6 +54,7 @@ def landing = config.landing
5054

5155
def hero
5256
div(class: "flex flex-col gap-6") do
57+
logo
5358
eyebrow
5459
heading
5560
lead
@@ -58,6 +63,22 @@ def hero
5863
end
5964
end
6065

66+
# The brand mark — an inline single-path SVG (currentColor, theme-adaptive) or
67+
# an <img>. Rendered above the eyebrow, like a product wordmark.
68+
def logo
69+
return unless (mark = landing.hero_logo)
70+
71+
if mark.inline?
72+
svg(viewbox: mark.viewbox, class: "h-9 w-auto text-primary", fill: "currentColor",
73+
role: "img", aria_label: mark.label) do |s|
74+
s.title { mark.label } if mark.label
75+
s.path(d: mark.svg)
76+
end
77+
else
78+
img(src: image_url(mark.src), alt: mark.alt.to_s, class: "h-9 w-auto")
79+
end
80+
end
81+
6182
def eyebrow
6283
return unless (text = landing.eyebrow)
6384

docs/config/initializers/docs_kit.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,16 @@
5454
# lambda instead; it wins.
5555
c.nav_registries = { "Docs" => Doc }
5656

57-
# The landing page (DocsUI::Landing), dogfooded — a hero + feature grid + a
58-
# registry-grouped doc index, all from config. A **run** in the title renders
59-
# in the primary color. See LandingsController#show (render_page).
57+
# The landing page (DocsUI::Landing), dogfooded — a hero (with a brand mark) +
58+
# feature grid + a registry-grouped doc index, all from config. A **run** in
59+
# the title renders in the primary color. See LandingsController#show.
60+
# The logo is an inline single-path SVG mark (a stacked-docs glyph) rendered
61+
# with fill: currentColor, so it adapts to every theme — dogfoods c.landing.logo.
62+
c.landing.logo = {
63+
svg: "M4 2h9l5 5v13a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm8 1.5V8h4.5L12 3.5z",
64+
viewbox: "0 0 22 24",
65+
label: "docs-kit"
66+
}
6067
c.landing.eyebrow = "docs-kit"
6168
c.landing.title = "Shared docs chrome for **Rails**, in Phlex."
6269
c.landing.lead = "The shell, sidebar, theme switcher, syntax highlighting, " \

docs/spec/system/docs_chrome_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
it "renders the landing page hero and the docs index" do
1111
visit "/"
1212

13+
# The hero brand mark (c.landing.logo, an inline currentColor SVG).
14+
expect(page).to have_css("svg[aria-label='docs-kit']")
1315
expect(page).to have_css("h1", text: "Shared docs chrome for Rails")
1416
expect(page).to have_link("Get started", href: "/docs/overview")
1517
# The drawer shell + sidebar are present (daisyUI Drawer).

lib/docs_kit/landing_config.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ module DocsKit
2626
# individually assignable in the `c.landing.x = ...` block, mirroring
2727
# DocsKit::SeoConfig.
2828
class LandingConfig
29+
# An optional brand logo/mark rendered at the top of the hero (above the
30+
# eyebrow), like a product wordmark. A Hash in one of two forms:
31+
# { svg: "<path d …>", viewbox: "0 0 81 45", label: "Brand" } # inline mark
32+
# { src: "logo.svg", alt: "Brand" } # image asset/URL
33+
# The inline `svg` form renders with `fill: currentColor` so it adapts to the
34+
# theme (light/dark) — best for a single-color mark. nil omits the logo.
35+
# See #hero_logo (the normalized Logo value object) and DocsUI::Landing.
36+
attr_writer :logo
37+
2938
# A small uppercase kicker above the title (e.g. "Developer Docs"). nil omits it.
3039
attr_accessor :eyebrow
3140

@@ -81,6 +90,13 @@ def install_snippet
8190
{ code: attrs[:code].to_s, filename: attrs[:filename], lexer: (attrs[:lexer] || :shell).to_sym }
8291
end
8392

93+
# The hero logo as a normalized Logo value object, or nil when unset.
94+
def hero_logo
95+
return if @logo.nil?
96+
97+
Logo.from(@logo)
98+
end
99+
84100
# One hero call-to-action button. `style` maps to a daisyUI btn variant
85101
# (:primary → btn-primary, anything else → btn-ghost). `icon` is an optional
86102
# brand/lucide token rendered before the label (DocsUI::BrandMark resolves it).
@@ -117,5 +133,28 @@ def self.from(feature)
117133
new(icon: attrs[:icon], title: attrs[:title], body: attrs[:body])
118134
end
119135
end
136+
137+
# The hero brand logo — either an inline single-path SVG mark (`svg` = the
138+
# `<path d>` data, `viewbox` = its viewBox) rendered with fill: currentColor so
139+
# it adapts to the theme, OR an image (`src` = an asset path/URL, `alt` = its
140+
# accessible name). `label` is the accessible name for the inline mark.
141+
Logo = Data.define(:svg, :viewbox, :src, :alt, :label) do
142+
def initialize(svg: nil, viewbox: "0 0 24 24", src: nil, alt: nil, label: nil)
143+
super
144+
end
145+
146+
def self.from(logo)
147+
return logo if logo.is_a?(self)
148+
149+
attrs = logo.to_h.transform_keys(&:to_sym)
150+
new(
151+
svg: attrs[:svg], viewbox: attrs[:viewbox] || "0 0 24 24",
152+
src: attrs[:src], alt: attrs[:alt], label: attrs[:label]
153+
)
154+
end
155+
156+
# An inline SVG mark (vs. an <img>). True when `svg` path data is present.
157+
def inline? = !svg.to_s.empty?
158+
end
120159
end
121160
end

lib/generators/docs_kit/install/templates/docs_kit.rb.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ Rails.application.config.to_prepare do
109109
# knobs. Every field is optional; with none set it still renders a minimal hero
110110
# (the brand + the doc index). Wrap a run in **double asterisks** to accent it
111111
# in the primary color.
112+
# c.landing.logo = { svg: "M4 2h9l5 5…Z", viewbox: "0 0 22 24", label: "<%= app_brand %>" }
113+
# # ^ an inline single-path SVG mark (fill: currentColor, theme-adaptive), OR
114+
# # an image: { src: "logo.svg", alt: "<%= app_brand %>" }
112115
# c.landing.eyebrow = "Developer Docs"
113116
# c.landing.title = "The <%= app_brand %> **API**"
114117
# c.landing.lead = "One sentence on what your product does."

spec/docs_kit/landing_config_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,36 @@
105105
expect(landing.install_snippet).to be_nil
106106
end
107107
end
108+
109+
describe "#hero_logo" do
110+
it "is nil when unset" do
111+
expect(landing.hero_logo).to be_nil
112+
end
113+
114+
it "normalizes an inline SVG-mark Hash into an inline Logo" do
115+
landing.logo = { svg: "M1 1H2Z", viewbox: "0 0 10 10", label: "Acme" }
116+
117+
logo = landing.hero_logo
118+
expect(logo).to be_a(described_class::Logo)
119+
expect(logo).to be_inline
120+
expect(logo.svg).to eq("M1 1H2Z")
121+
expect(logo.viewbox).to eq("0 0 10 10")
122+
expect(logo.label).to eq("Acme")
123+
end
124+
125+
it "normalizes an image Hash into a non-inline Logo" do
126+
landing.logo = { src: "logo.svg", alt: "Acme" }
127+
128+
logo = landing.hero_logo
129+
expect(logo).not_to be_inline
130+
expect(logo.src).to eq("logo.svg")
131+
expect(logo.alt).to eq("Acme")
132+
end
133+
134+
it "defaults the viewbox to a 24×24 box" do
135+
landing.logo = { svg: "M0 0Z" }
136+
137+
expect(landing.hero_logo.viewbox).to eq("0 0 24 24")
138+
end
139+
end
108140
end

0 commit comments

Comments
 (0)