From 2c88990696ef173d9616e282ffa69f86a824e0c4 Mon Sep 17 00:00:00 2001 From: mhenrixon Date: Sat, 4 Jul 2026 17:06:51 +0200 Subject: [PATCH] feat(landing): optional hero logo for DocsUI::Landing (c.landing.logo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: "", 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. --- .rubocop.yml | 4 ++ CHANGELOG.md | 7 ++++ app/components/docs_ui/landing.rb | 29 ++++++++++++-- docs/config/initializers/docs_kit.rb | 13 +++++-- docs/spec/system/docs_chrome_spec.rb | 2 + lib/docs_kit/landing_config.rb | 39 +++++++++++++++++++ .../install/templates/docs_kit.rb.erb | 3 ++ spec/docs_kit/landing_config_spec.rb | 32 +++++++++++++++ 8 files changed, 122 insertions(+), 7 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 3a1cf93..3f546d1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2794aa8..804eae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: "", 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 diff --git a/app/components/docs_ui/landing.rb b/app/components/docs_ui/landing.rb index de0f1c0..df31df8 100644 --- a/app/components/docs_ui/landing.rb +++ b/app/components/docs_ui/landing.rb @@ -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." @@ -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 @@ -50,6 +54,7 @@ def landing = config.landing def hero div(class: "flex flex-col gap-6") do + logo eyebrow heading lead @@ -58,6 +63,22 @@ def hero end end + # The brand mark — an inline single-path SVG (currentColor, theme-adaptive) or + # an . 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) diff --git a/docs/config/initializers/docs_kit.rb b/docs/config/initializers/docs_kit.rb index e7bf408..d90c825 100644 --- a/docs/config/initializers/docs_kit.rb +++ b/docs/config/initializers/docs_kit.rb @@ -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, " \ diff --git a/docs/spec/system/docs_chrome_spec.rb b/docs/spec/system/docs_chrome_spec.rb index ff9ae98..331e039 100644 --- a/docs/spec/system/docs_chrome_spec.rb +++ b/docs/spec/system/docs_chrome_spec.rb @@ -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). diff --git a/lib/docs_kit/landing_config.rb b/lib/docs_kit/landing_config.rb index 10aa21c..fa20af0 100644 --- a/lib/docs_kit/landing_config.rb +++ b/lib/docs_kit/landing_config.rb @@ -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: "", 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 @@ -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). @@ -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 + # `` 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 ). True when `svg` path data is present. + def inline? = !svg.to_s.empty? + end end end diff --git a/lib/generators/docs_kit/install/templates/docs_kit.rb.erb b/lib/generators/docs_kit/install/templates/docs_kit.rb.erb index 6e07e0f..8300f12 100644 --- a/lib/generators/docs_kit/install/templates/docs_kit.rb.erb +++ b/lib/generators/docs_kit/install/templates/docs_kit.rb.erb @@ -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." diff --git a/spec/docs_kit/landing_config_spec.rb b/spec/docs_kit/landing_config_spec.rb index 7182702..cc7664f 100644 --- a/spec/docs_kit/landing_config_spec.rb +++ b/spec/docs_kit/landing_config_spec.rb @@ -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