Skip to content

feat(shell): config-driven brand mark — c.brand_logo + c.topbar_brand dedup - #68

Merged
mhenrixon merged 1 commit into
mainfrom
issue-64-brand-logo
Aug 4, 2026
Merged

feat(shell): config-driven brand mark — c.brand_logo + c.topbar_brand dedup#68
mhenrixon merged 1 commit into
mainfrom
issue-64-brand-logo

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

Closes #64. Implements the plan in #64 (comment).

Summary

Sites wanting their own mark in the shell chrome had to prepend copies of Shell#topbar / Sidebar#header_section — copies that silently went stale on upgrades (a 1.0.6-era copy swallowed 1.0.7's app_home_link). This PR makes the mark a config knob:

  • c.brand_logo — a DocsKit::BrandLogo value object accepting exactly one of five forms, rendered by the new shared DocsUI::Logo component inside both brand anchors (topbar h-6, sidebar h-7), falling back to the text c.brand. Unset config renders byte-identical markup.

    Form Renders as
    svg: path-d (landing-compat) inline <svg fill="currentColor">, theme-adaptive
    paths: [d1, d2, …] same, multi-path wordmark
    markup: full <svg> string embedded verbatim in a sized wrapper
    file: path to a .svg file content embedded inline, re-read on mtime change
    src: asset path/URL <img> via image_url (not theme-adaptive, documented)
  • c.topbar_brand:always (default, strict byte-compat) or :mobile_only, which adds lg:hidden to the topbar brand anchor: at the drawer-pinned breakpoint the sidebar brand is already visible, so the topbar copy is a duplicate.

  • Landing unificationLanding#logo now renders through the same DocsUI::Logo; LandingConfig#hero_logo normalizes through DocsKit::BrandLogo (LandingConfig::Logo stays as a constant alias), so c.landing.logo gains all five forms for free.

Escaping posture: svg:/paths: render every path-d as an ordinary Phlex-escaped attribute (covered by a spec asserting an injected " cannot break out). markup:/file: deliberately embed site-authored markup — the site's own initializer/asset, the same trust domain as its own views — and both are shape-checked to be an <svg> element at config time with a loud ArgumentError otherwise.

Generator: the initializer template documents both knobs (commented, opt-in); docs-kit new runs the same generator. README gains a "The brand mark" section.

Test plan

  • spec/docs_kit/brand_logo_spec.rb — the five forms, exactly-one-form + <svg> shape guards, label/alt fallback, file mtime memoization + invalidation
  • spec/docs_kit/configuration_spec.rbbrand_logo nil default, memoized normalization, loud failure on malformed values; topbar_brand default/validation
  • spec/docs_ui/logo_spec.rb — per-form rendering, path-d attribute escaping, role="img"/aria-label/<title>, sized embed wrapper, raw-src degrade off-request
  • spec/docs_ui/shell_spec.rb / sidebar_spec.rb — byte-compat when unset (no <svg> in the brand anchor, no lg:hidden), mark inside the anchor, c.brand aria fallback, lg:hidden only on :mobile_only, version badge kept
  • spec/docs_ui/landing_spec.rb — hero-logo semantics pinned across the unification
  • spec/generators/install_generator_spec.rb — the generated initializer documents both knobs

bundle exec rake: 855 examples green, RuboCop clean, coverage above the 80% floor. All sizing classes (h-6/h-7/w-auto/lg:hidden, the [&>svg]:* wrapper variants) are literals in scanned gem .rb files — no @source inline addition needed.

Deviations & judgment calls

Deviations

  • The plan said "keep LandingConfig::Logo as an alias if anything references it" — it IS referenced (landing_config_spec asserts the type), so the nested Data class was replaced with Logo = DocsKit::BrandLogo. That required brand_logo.rb to be eagerly required from landing_config.rb and loader.ignored in lib/docs_kit.rb (the alias resolves at require time, before zeitwerk is set up) — the existing seo_config/landing_config pattern.

Discoveries (behavior deltas folded into the unification)

  • The worktree base (f942145, PR feat(sidebar): flatten the top-level nav heading #66 sidebar flattening) is newer than the plan's investigation snapshot; Sidebar#header_section and Landing#logo were unchanged, so the plan held.
  • Landing's src: image form previously raised NoMethodError in a view-context-less render (image_url on a nil view_context). The shared renderer degrades to the raw src off-request — the DocsUI::MetaTags posture — so that latent bug is fixed as a side effect.
  • Landing's inline hero svg emitted a lowercase viewbox attribute; the shared renderer emits the correct viewBox casing (the BrandMark posture). Browsers normalize both identically, but the emitted bytes differ for sites using c.landing.logo's inline form (semantics pinned by spec).
  • The landing hero now passes h-9 w-auto text-primary to BOTH forms; the old code gave the <img> form h-9 w-auto only. text-primary on an <img> is inert.

Judgment calls

  • BrandLogo.from raises ArgumentError on an empty/ambiguous Hash (no form key, or several mixed). The old LandingConfig::Logo.from silently built an all-nil value that rendered a broken <img src="">. Loud-at-config-time matches the on_page/openapi_document posture but is stricter than the old landing behavior.
  • label/alt fall back to each other so a site setting either gets an accessible name in every render form; both unset falls back to c.brand at render time.
  • Configuration#brand_logo memoizes its normalized value (invalidated on reassignment), unlike #app_link's rebuild-per-read — a file: mark reads and shape-checks its SVG on build, so per-render re-normalization would repeat IO. RuboCop's memoized-ivar-name cop forced the @brand_logo_raw (writer) / @brand_logo (memo) naming.
  • The markup:/file: embed renders inside a span wrapper carrying the caller's sizing plus literal inline-flex [&>svg]:h-full [&>svg]:w-auto, so the site's own <svg> fills the surface without its markup being rewritten.
  • A file: mark validates (exists, .svg extension, <svg shape) eagerly at normalize time and re-reads on mtime change — the openapi_document posture.

…bar, c.topbar_brand dedup

## Summary
Sites wanting their own mark in the shell chrome had to prepend copies of
Shell#topbar / Sidebar#header_section, which silently went stale on upgrades.
Now a DocsKit::BrandLogo value object (five forms: svg:/paths: inline path-d,
markup:/file: verbatim site-authored <svg>, src: <img>) renders through one
shared DocsUI::Logo component inside both brand anchors, falling back to the
text c.brand — unset config stays byte-identical. c.topbar_brand = :mobile_only
optionally hides the desktop topbar brand (lg:hidden) where the pinned sidebar
already shows it; the default :always keeps today's markup verbatim.

The landing hero logo unifies onto the same value object + renderer
(LandingConfig::Logo is now an alias), which also fixes its src: form raising
off-request and adopts the correct viewBox attribute casing.

## Test Coverage
- brand_logo_spec: the five forms, exactly-one-form + <svg> shape guards,
  label/alt fallback, file mtime memoization/invalidation
- configuration_spec: brand_logo nil default + memoized normalization;
  topbar_brand :always default + validation
- logo_spec: per-form rendering, path-d attribute escaping, aria-label/<title>,
  sized embed wrapper, raw-src degrade off-request
- shell_spec/sidebar_spec: byte-compat when unset, mark inside the brand
  anchor, brand-name aria fallback, lg:hidden only on :mobile_only,
  version badge kept
- landing_spec: hero-logo semantics pinned across the unification
- install_generator_spec: initializer documents both knobs

## Verification
- [x] bundle exec rubocop passes
- [x] bundle exec rspec passes (855 examples, coverage above the 80% floor)

Closes #64
@mhenrixon mhenrixon self-assigned this Aug 4, 2026
@mhenrixon mhenrixon added the enhancement New feature or request label Aug 4, 2026
@mhenrixon
mhenrixon merged commit 00b72b7 into main Aug 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shell brand logo: config knob for topbar/sidebar brand mark (and desktop topbar-brand dedup)

1 participant