Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ Metrics/ClassLength:
- "lib/docs_kit/configuration.rb"
- "lib/docs_kit/open_api/operation.rb"
- "lib/docs_kit/open_api/schema.rb"
# A composite renderer: logo + hero (eyebrow/title/lead/install/ctas) + a
# feature grid + a doc index. Each part is a small focused method; the class
# is long only because it renders several sections, like the OpenAPI ones above.
- "app/components/docs_ui/landing.rb"

Metrics/MethodLength:
Max: 25
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@

### Added

- **`DocsUI::Landing` hero logo (`c.landing.logo`).** The landing hero now takes
an optional brand mark above the eyebrow, in two forms: an inline single-path
SVG (`{ svg: "<path d>", viewbox:, label: }`, rendered with `fill: currentColor`
so it adapts to the theme) or an image (`{ src:, alt: }`, resolved through the
site's asset pipeline). nil (the default) omits it, so it's backwards-compatible.
This closes the gap a mounted docs app hit — a hero with no way to show the
product's own logo — the first consumer to try `DocsUI::Landing` on a real site.
- **`DocsUI::Landing` — a config-driven marketing landing page.** Every consuming
site (and this dogfood site) was hand-rolling a home page; now render
`DocsUI::Landing` and drive it from a new `c.landing` config block
Expand Down
29 changes: 25 additions & 4 deletions app/components/docs_ui/landing.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# frozen_string_literal: true

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

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

def hero
div(class: "flex flex-col gap-6") do
logo
eyebrow
heading
lead
Expand All @@ -58,6 +63,22 @@ def hero
end
end

# The brand mark — an inline single-path SVG (currentColor, theme-adaptive) or
# an <img>. Rendered above the eyebrow, like a product wordmark.
def logo
return unless (mark = landing.hero_logo)

if mark.inline?
svg(viewbox: mark.viewbox, class: "h-9 w-auto text-primary", fill: "currentColor",
role: "img", aria_label: mark.label) do |s|
s.title { mark.label } if mark.label
s.path(d: mark.svg)
end
else
img(src: image_url(mark.src), alt: mark.alt.to_s, class: "h-9 w-auto")
end
end

def eyebrow
return unless (text = landing.eyebrow)

Expand Down
13 changes: 10 additions & 3 deletions docs/config/initializers/docs_kit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,16 @@
# lambda instead; it wins.
c.nav_registries = { "Docs" => Doc }

# The landing page (DocsUI::Landing), dogfooded — a hero + feature grid + a
# registry-grouped doc index, all from config. A **run** in the title renders
# in the primary color. See LandingsController#show (render_page).
# The landing page (DocsUI::Landing), dogfooded — a hero (with a brand mark) +
# feature grid + a registry-grouped doc index, all from config. A **run** in
# the title renders in the primary color. See LandingsController#show.
# The logo is an inline single-path SVG mark (a stacked-docs glyph) rendered
# with fill: currentColor, so it adapts to every theme — dogfoods c.landing.logo.
c.landing.logo = {
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",
viewbox: "0 0 22 24",
label: "docs-kit"
}
c.landing.eyebrow = "docs-kit"
c.landing.title = "Shared docs chrome for **Rails**, in Phlex."
c.landing.lead = "The shell, sidebar, theme switcher, syntax highlighting, " \
Expand Down
2 changes: 2 additions & 0 deletions docs/spec/system/docs_chrome_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
it "renders the landing page hero and the docs index" do
visit "/"

# The hero brand mark (c.landing.logo, an inline currentColor SVG).
expect(page).to have_css("svg[aria-label='docs-kit']")
expect(page).to have_css("h1", text: "Shared docs chrome for Rails")
expect(page).to have_link("Get started", href: "/docs/overview")
# The drawer shell + sidebar are present (daisyUI Drawer).
Expand Down
39 changes: 39 additions & 0 deletions lib/docs_kit/landing_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ module DocsKit
# individually assignable in the `c.landing.x = ...` block, mirroring
# DocsKit::SeoConfig.
class LandingConfig
# An optional brand logo/mark rendered at the top of the hero (above the
# eyebrow), like a product wordmark. A Hash in one of two forms:
# { svg: "<path d …>", viewbox: "0 0 81 45", label: "Brand" } # inline mark
# { src: "logo.svg", alt: "Brand" } # image asset/URL
# The inline `svg` form renders with `fill: currentColor` so it adapts to the
# theme (light/dark) — best for a single-color mark. nil omits the logo.
# See #hero_logo (the normalized Logo value object) and DocsUI::Landing.
attr_writer :logo

# A small uppercase kicker above the title (e.g. "Developer Docs"). nil omits it.
attr_accessor :eyebrow

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

# The hero logo as a normalized Logo value object, or nil when unset.
def hero_logo
return if @logo.nil?

Logo.from(@logo)
end

# One hero call-to-action button. `style` maps to a daisyUI btn variant
# (:primary → btn-primary, anything else → btn-ghost). `icon` is an optional
# brand/lucide token rendered before the label (DocsUI::BrandMark resolves it).
Expand Down Expand Up @@ -117,5 +133,28 @@ def self.from(feature)
new(icon: attrs[:icon], title: attrs[:title], body: attrs[:body])
end
end

# The hero brand logo — either an inline single-path SVG mark (`svg` = the
# `<path d>` data, `viewbox` = its viewBox) rendered with fill: currentColor so
# it adapts to the theme, OR an image (`src` = an asset path/URL, `alt` = its
# accessible name). `label` is the accessible name for the inline mark.
Logo = Data.define(:svg, :viewbox, :src, :alt, :label) do
def initialize(svg: nil, viewbox: "0 0 24 24", src: nil, alt: nil, label: nil)
super
end

def self.from(logo)
return logo if logo.is_a?(self)

attrs = logo.to_h.transform_keys(&:to_sym)
new(
svg: attrs[:svg], viewbox: attrs[:viewbox] || "0 0 24 24",
src: attrs[:src], alt: attrs[:alt], label: attrs[:label]
)
end

# An inline SVG mark (vs. an <img>). True when `svg` path data is present.
def inline? = !svg.to_s.empty?
end
end
end
3 changes: 3 additions & 0 deletions lib/generators/docs_kit/install/templates/docs_kit.rb.erb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ Rails.application.config.to_prepare do
# knobs. Every field is optional; with none set it still renders a minimal hero
# (the brand + the doc index). Wrap a run in **double asterisks** to accent it
# in the primary color.
# c.landing.logo = { svg: "M4 2h9l5 5…Z", viewbox: "0 0 22 24", label: "<%= app_brand %>" }
# # ^ an inline single-path SVG mark (fill: currentColor, theme-adaptive), OR
# # an image: { src: "logo.svg", alt: "<%= app_brand %>" }
# c.landing.eyebrow = "Developer Docs"
# c.landing.title = "The <%= app_brand %> **API**"
# c.landing.lead = "One sentence on what your product does."
Expand Down
32 changes: 32 additions & 0 deletions spec/docs_kit/landing_config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,36 @@
expect(landing.install_snippet).to be_nil
end
end

describe "#hero_logo" do
it "is nil when unset" do
expect(landing.hero_logo).to be_nil
end

it "normalizes an inline SVG-mark Hash into an inline Logo" do
landing.logo = { svg: "M1 1H2Z", viewbox: "0 0 10 10", label: "Acme" }

logo = landing.hero_logo
expect(logo).to be_a(described_class::Logo)
expect(logo).to be_inline
expect(logo.svg).to eq("M1 1H2Z")
expect(logo.viewbox).to eq("0 0 10 10")
expect(logo.label).to eq("Acme")
end

it "normalizes an image Hash into a non-inline Logo" do
landing.logo = { src: "logo.svg", alt: "Acme" }

logo = landing.hero_logo
expect(logo).not_to be_inline
expect(logo.src).to eq("logo.svg")
expect(logo.alt).to eq("Acme")
end

it "defaults the viewbox to a 24×24 box" do
landing.logo = { svg: "M0 0Z" }

expect(landing.hero_logo.viewbox).to eq("0 0 24 24")
end
end
end
Loading