feat(shell): config-driven brand mark — c.brand_logo + c.topbar_brand dedup - #68
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'sapp_home_link). This PR makes the mark a config knob:c.brand_logo— aDocsKit::BrandLogovalue object accepting exactly one of five forms, rendered by the new sharedDocsUI::Logocomponent inside both brand anchors (topbarh-6, sidebarh-7), falling back to the textc.brand. Unset config renders byte-identical markup.svg:path-d (landing-compat)<svg fill="currentColor">, theme-adaptivepaths:[d1, d2, …]markup:full<svg>stringfile:path to a.svgsrc:asset path/URL<img>viaimage_url(not theme-adaptive, documented)c.topbar_brand—:always(default, strict byte-compat) or:mobile_only, which addslg:hiddento the topbar brand anchor: at the drawer-pinned breakpoint the sidebar brand is already visible, so the topbar copy is a duplicate.Landing unification —
Landing#logonow renders through the sameDocsUI::Logo;LandingConfig#hero_logonormalizes throughDocsKit::BrandLogo(LandingConfig::Logostays as a constant alias), soc.landing.logogains 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 loudArgumentErrorotherwise.Generator: the initializer template documents both knobs (commented, opt-in);
docs-kit newruns 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 + invalidationspec/docs_kit/configuration_spec.rb—brand_logonil default, memoized normalization, loud failure on malformed values;topbar_branddefault/validationspec/docs_ui/logo_spec.rb— per-form rendering, path-d attribute escaping,role="img"/aria-label/<title>, sized embed wrapper, raw-src degrade off-requestspec/docs_ui/shell_spec.rb/sidebar_spec.rb— byte-compat when unset (no<svg>in the brand anchor, nolg:hidden), mark inside the anchor,c.brandaria fallback,lg:hiddenonly on:mobile_only, version badge keptspec/docs_ui/landing_spec.rb— hero-logo semantics pinned across the unificationspec/generators/install_generator_spec.rb— the generated initializer documents both knobsbundle 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.rbfiles — no@source inlineaddition needed.Deviations & judgment calls
Deviations
LandingConfig::Logoas an alias if anything references it" — it IS referenced (landing_config_specasserts the type), so the nested Data class was replaced withLogo = DocsKit::BrandLogo. That requiredbrand_logo.rbto be eagerly required fromlanding_config.rbandloader.ignored inlib/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)
f942145, PR feat(sidebar): flatten the top-level nav heading #66 sidebar flattening) is newer than the plan's investigation snapshot;Sidebar#header_sectionandLanding#logowere unchanged, so the plan held.src:image form previously raisedNoMethodErrorin a view-context-less render (image_urlon a nil view_context). The shared renderer degrades to the raw src off-request — theDocsUI::MetaTagsposture — so that latent bug is fixed as a side effect.viewboxattribute; the shared renderer emits the correctviewBoxcasing (theBrandMarkposture). Browsers normalize both identically, but the emitted bytes differ for sites usingc.landing.logo's inline form (semantics pinned by spec).h-9 w-auto text-primaryto BOTH forms; the old code gave the<img>formh-9 w-autoonly.text-primaryon an<img>is inert.Judgment calls
BrandLogo.fromraisesArgumentErroron an empty/ambiguous Hash (no form key, or several mixed). The oldLandingConfig::Logo.fromsilently built an all-nil value that rendered a broken<img src="">. Loud-at-config-time matches theon_page/openapi_documentposture but is stricter than the old landing behavior.label/altfall back to each other so a site setting either gets an accessible name in every render form; both unset falls back toc.brandat render time.Configuration#brand_logomemoizes its normalized value (invalidated on reassignment), unlike#app_link's rebuild-per-read — afile: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.markup:/file:embed renders inside aspanwrapper carrying the caller's sizing plus literalinline-flex [&>svg]:h-full [&>svg]:w-auto, so the site's own<svg>fills the surface without its markup being rewritten.file:mark validates (exists,.svgextension,<svgshape) eagerly at normalize time and re-reads on mtime change — theopenapi_documentposture.