docs-kit 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +39 -0
- data/app/components/docs_ui/landing.rb +9 -18
- data/app/components/docs_ui/logo.rb +74 -0
- data/app/components/docs_ui/shell.rb +21 -1
- data/app/components/docs_ui/sidebar.rb +30 -14
- data/lib/docs_kit/brand_logo.rb +124 -0
- data/lib/docs_kit/configuration.rb +47 -0
- data/lib/docs_kit/landing_config.rb +8 -24
- data/lib/docs_kit/version.rb +1 -1
- data/lib/docs_kit.rb +3 -0
- data/lib/generators/docs_kit/install/templates/docs_kit.rb.erb +17 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78cf5f2d5a4636b8b0c603f3d3b238aeb68c100fd0133edb16d52ce56ade4c39
|
|
4
|
+
data.tar.gz: 50acc90365594b546ba40887c976eb6f7c6e1eca7389b834c6793cd0490bbfb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b650c98a593eda398bafde41a07ed5ad01325d5392b113a8cfebedc06bb71519e139f846b8e6e259c106c8b23a64a2af4c9a0ab5ab1efd1b2b524764be72fa07
|
|
7
|
+
data.tar.gz: 29c352545d1ffcd4ba93461b2df5582663b79d86db835866fc6b2d543018d5d2e0034e43f24f2f67d0ebd953b89e65788a276484901c71b693fe95040668b8ad
|
data/README.md
CHANGED
|
@@ -166,6 +166,10 @@ DocsKit.configure do |c|
|
|
|
166
166
|
# once in the topbar, right after the brand. Unset (default) renders nothing.
|
|
167
167
|
c.app_link = { href: "/", label: "Back to the app" }
|
|
168
168
|
|
|
169
|
+
# Your own mark in the topbar + sidebar header instead of the text brand.
|
|
170
|
+
c.brand_logo = { paths: ["M4 2h9l5 5…Z"], viewbox: "0 0 81 45" }
|
|
171
|
+
c.topbar_brand = :mobile_only # drop the desktop duplicate (default :always)
|
|
172
|
+
|
|
169
173
|
# Repo/social links in the topbar (next to the theme switcher).
|
|
170
174
|
c.topbar_links = [
|
|
171
175
|
{ href: "https://github.com/you/phlex-reactive", label: "GitHub", icon: :github },
|
|
@@ -185,6 +189,12 @@ registry maps a heading to its authored pages (`Doc.nav_items`); a page that
|
|
|
185
189
|
isn't written yet is skipped, so there are no dead links. Register a page with
|
|
186
190
|
one line (see [Add a page](#add-a-page)) and it appears in the sidebar.
|
|
187
191
|
|
|
192
|
+
The registry's page groups ("Getting started", "REST API", …) are the top level
|
|
193
|
+
of the rendered menu — each an open, collapsible section. The heading above them
|
|
194
|
+
only appears when you register **several** headings, and then as a static label
|
|
195
|
+
(no fold): a site with one registry gets no redundant "Documentation" level, and
|
|
196
|
+
nothing in the sidebar is indented deeper than group → page.
|
|
197
|
+
|
|
188
198
|
### The two homes, the brand link, and dark code themes
|
|
189
199
|
|
|
190
200
|
These knobs cover what sites used to shim by subclassing `DocsUI::Shell` or
|
|
@@ -194,6 +204,8 @@ overriding route helpers:
|
|
|
194
204
|
|------|---------|--------------|
|
|
195
205
|
| `c.brand_href` | `"/"` | The **docs home** — the href of the topbar brand, the sidebar brand, and each page's "← Docs home" masthead link. Set it (e.g. `"/docs"`) when the docs live under a subpath, instead of subclassing `Shell` or overriding `root_path`. |
|
|
196
206
|
| `c.app_link` | `nil` | The **app home** — an opt-in `{ href:, label: }` link back to the application hosting the docs, rendered once in the topbar right after the brand (e.g. `{ href: "/", label: "Back to the app" }`). Unset renders nothing, so a standalone docs site is unchanged. External hrefs open in a new tab with `rel=noopener`. |
|
|
207
|
+
| `c.brand_logo` | `nil` | Your **brand mark**, rendered inside the brand anchor of BOTH the topbar and the sidebar header in place of the text `c.brand` (which stays the accessible name / `aria-label` fallback). Unset renders the text brand, byte-identical to before. Takes exactly one of five forms — see [The brand mark](#the-brand-mark) below. |
|
|
208
|
+
| `c.topbar_brand` | `:always` | Where the topbar renders the brand. At the drawer-pinned breakpoint (`lg:`) the sidebar brand is always visible, so the topbar copy is a duplicate — `:mobile_only` hides it there (`lg:hidden`). The default keeps today's markup verbatim. |
|
|
197
209
|
| `c.code_theme_dark` | `nil` | A second Rouge theme for **dark** daisyUI themes. `nil` keeps the single-theme behavior (fully backwards compatible). When set, `DocsUI::Code` also emits this theme's CSS scoped under `[data-theme=X] .code-highlight` for each shipped dark theme, so code blocks stay readable when the switcher flips to a dark theme. |
|
|
198
210
|
| `c.dark_themes` | daisyUI's built-in dark theme names | Which theme names count as dark for `code_theme_dark`. Intersected with `c.themes` at render time, so only shipped themes emit CSS. Override to name custom dark themes (e.g. `%w[zazu-dark]`). |
|
|
199
211
|
|
|
@@ -203,6 +215,33 @@ blocks with no JavaScript and no flash. The Rouge CSS is inlined per block
|
|
|
203
215
|
(not part of the Tailwind build), so the [theme-sync invariant](#css--the-canonical-build)
|
|
204
216
|
is unaffected — a `code_theme_dark` doesn't need a CSS rebuild.
|
|
205
217
|
|
|
218
|
+
### The brand mark
|
|
219
|
+
|
|
220
|
+
`c.brand_logo` replaces the text brand in the shell chrome (topbar + sidebar
|
|
221
|
+
header) with your own mark — no more copying private `Shell`/`Sidebar` methods
|
|
222
|
+
that go stale on upgrades. It takes **exactly one** of five forms (mixing forms,
|
|
223
|
+
or a malformed value, raises at config time):
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
c.brand_logo = { svg: "M4 2h9l5 5…Z", viewbox: "0 0 22 24", label: "Acme" } # one path-d
|
|
227
|
+
c.brand_logo = { paths: ["M4 2…Z", "M9 7…Z"], viewbox: "0 0 81 45" } # multi-path wordmark
|
|
228
|
+
c.brand_logo = { markup: File.read("mark.svg") } # raw <svg> markup, embedded verbatim
|
|
229
|
+
c.brand_logo = { file: "app/assets/images/mark.svg" } # a .svg file, embedded inline
|
|
230
|
+
c.brand_logo = { src: "logo.png", alt: "Acme" } # an <img> from the asset pipeline
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- The `svg:`/`paths:` forms render with `fill="currentColor"`, so the mark
|
|
234
|
+
**recolors with the active daisyUI theme**. `markup:`/`file:` are embedded
|
|
235
|
+
as-authored — use `currentColor` inside them to stay theme-adaptive. An
|
|
236
|
+
`src:` `<img>` **cannot** inherit `currentColor` and won't adapt.
|
|
237
|
+
- `markup:`/`file:` embed your own SVG verbatim (they are your site's content,
|
|
238
|
+
same trust domain as your views); both are shape-checked to be an `<svg>`
|
|
239
|
+
element at config time, and a `file:` re-reads on change in development.
|
|
240
|
+
- `label:`/`alt:` name the mark for assistive tech; unset, the mark falls back
|
|
241
|
+
to `c.brand`. The sidebar keeps the `version_badge` next to the mark.
|
|
242
|
+
- Sizing is fixed per surface (topbar `h-6`, sidebar `h-7`, landing hero `h-9`).
|
|
243
|
+
- `c.landing.logo` (the landing-hero mark) accepts the same five forms.
|
|
244
|
+
|
|
206
245
|
### Topbar links (repo & social)
|
|
207
246
|
|
|
208
247
|
Point readers at your source repo, chat, or socials from the topbar (next to the
|
|
@@ -31,9 +31,6 @@ module DocsUI
|
|
|
31
31
|
# walks the same #docs-content region Shell stamps.
|
|
32
32
|
class Landing < Phlex::HTML
|
|
33
33
|
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
|
|
37
34
|
|
|
38
35
|
def view_template
|
|
39
36
|
render DocsUI::Shell.new(title: landing.eyebrow || config.brand) do
|
|
@@ -63,20 +60,12 @@ module DocsUI
|
|
|
63
60
|
end
|
|
64
61
|
end
|
|
65
62
|
|
|
66
|
-
# The brand mark —
|
|
67
|
-
#
|
|
63
|
+
# The brand mark — the shared DocsUI::Logo renderer (any DocsKit::BrandLogo
|
|
64
|
+
# form) at hero size. Rendered above the eyebrow, like a product wordmark.
|
|
68
65
|
def logo
|
|
69
66
|
return unless (mark = landing.hero_logo)
|
|
70
67
|
|
|
71
|
-
|
|
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
|
|
68
|
+
render DocsUI::Logo.new(mark, class: "h-9 w-auto text-primary")
|
|
80
69
|
end
|
|
81
70
|
|
|
82
71
|
def eyebrow
|
|
@@ -162,15 +151,17 @@ module DocsUI
|
|
|
162
151
|
div(class: "mt-16") do
|
|
163
152
|
h2(class: "text-sm font-semibold uppercase tracking-wide text-base-content/50") { "Documentation" }
|
|
164
153
|
div(class: "mt-6 grid gap-8 sm:grid-cols-2") do
|
|
165
|
-
|
|
154
|
+
# A lone heading would just repeat the h2 above (often literally
|
|
155
|
+
# "Documentation"), so only label the columns when there are several.
|
|
156
|
+
groups.each { |heading, items| doc_index_group(heading, items, labeled: groups.size > 1) }
|
|
166
157
|
end
|
|
167
158
|
end
|
|
168
159
|
end
|
|
169
160
|
|
|
170
|
-
def doc_index_group(heading, items)
|
|
161
|
+
def doc_index_group(heading, items, labeled:)
|
|
171
162
|
div do
|
|
172
|
-
h3(class: "text-xs font-semibold uppercase tracking-wide text-base-content/40") { heading }
|
|
173
|
-
ul(class: "mt-3 flex flex-col gap-2") do
|
|
163
|
+
h3(class: "text-xs font-semibold uppercase tracking-wide text-base-content/40") { heading } if labeled
|
|
164
|
+
ul(class: labeled ? "mt-3 flex flex-col gap-2" : "flex flex-col gap-2") do
|
|
174
165
|
items.each { |item| li { a(href: item.href, class: "link link-hover text-sm") { item.label } } }
|
|
175
166
|
end
|
|
176
167
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# Renders a DocsKit::BrandLogo (config.brand_logo / config.landing.logo) as the
|
|
5
|
+
# brand mark — an inline currentColor <svg> (theme-adaptive), a verbatim
|
|
6
|
+
# site-authored <svg> embed, or an <img>.
|
|
7
|
+
#
|
|
8
|
+
# render DocsUI::Logo.new(config.brand_logo, class: "h-6 w-auto", label: config.brand)
|
|
9
|
+
#
|
|
10
|
+
# label: is the accessible-name fallback when the logo itself carries none
|
|
11
|
+
# (callers pass config.brand, so the mark always announces the site).
|
|
12
|
+
#
|
|
13
|
+
# The svg:/paths: forms emit each path-d as an ordinary Phlex-escaped
|
|
14
|
+
# attribute — config free text never bypasses the escape. The markup:/file:
|
|
15
|
+
# forms DO embed markup verbatim via raw(safe(...)): that content is the
|
|
16
|
+
# site's own deliberately-configured SVG (its initializer / its asset file —
|
|
17
|
+
# the same trust domain as the site's own views, which can already render
|
|
18
|
+
# anything), not third-party free text, and DocsKit::BrandLogo shape-checks it
|
|
19
|
+
# to be an <svg> element at config time. That authored-by-the-trusting-site
|
|
20
|
+
# rationale is the same carve-out DocsUI::BrandMark uses for its gem-authored
|
|
21
|
+
# path constants.
|
|
22
|
+
class Logo < Phlex::HTML
|
|
23
|
+
# For the src: image form — resolve the asset path through the site's
|
|
24
|
+
# pipeline to its digested /assets URL, exactly like DocsUI::Landing's img.
|
|
25
|
+
include Phlex::Rails::Helpers::ImageURL
|
|
26
|
+
|
|
27
|
+
def initialize(logo, label: nil, **attributes)
|
|
28
|
+
@logo = DocsKit::BrandLogo.from(logo)
|
|
29
|
+
@label = label
|
|
30
|
+
@attributes = attributes
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def view_template
|
|
34
|
+
if @logo.image?
|
|
35
|
+
img(src: resolved_src, alt: accessible_name.to_s, **@attributes)
|
|
36
|
+
elsif @logo.embed?
|
|
37
|
+
embedded_svg
|
|
38
|
+
else
|
|
39
|
+
inline_svg
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def accessible_name = @logo.label || @label
|
|
46
|
+
|
|
47
|
+
# The svg:/paths: forms: a currentColor mark that recolors with the active
|
|
48
|
+
# daisyUI theme; every path-d is an escaped attribute value.
|
|
49
|
+
def inline_svg
|
|
50
|
+
svg(viewBox: @logo.viewbox, fill: "currentColor", role: "img",
|
|
51
|
+
aria_label: accessible_name, **@attributes) do |s|
|
|
52
|
+
s.title { accessible_name } if accessible_name
|
|
53
|
+
@logo.paths.each { |d| s.path(d: d) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The markup:/file: forms: the site's own <svg> embedded verbatim (see the
|
|
58
|
+
# class comment for the trust rationale) inside a wrapper that carries the
|
|
59
|
+
# caller's sizing — the inner svg fills it. Literal arbitrary variants so
|
|
60
|
+
# Tailwind scans them from this file.
|
|
61
|
+
def embedded_svg
|
|
62
|
+
classes = [@attributes[:class], "inline-flex [&>svg]:h-full [&>svg]:w-auto"].compact.join(" ")
|
|
63
|
+
span(role: "img", aria_label: accessible_name, **@attributes, class: classes) do
|
|
64
|
+
raw(safe(@logo.svg_markup))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The digested asset URL when a view context is present; off a request (an
|
|
69
|
+
# isolated render) degrade to the raw src — the DocsUI::MetaTags posture.
|
|
70
|
+
def resolved_src
|
|
71
|
+
view_context ? image_url(@logo.src) : @logo.src
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -156,7 +156,7 @@ module DocsUI
|
|
|
156
156
|
div(class: "flex-1 items-center gap-2") do
|
|
157
157
|
label(for: DRAWER_ID, class: "btn btn-square btn-ghost btn-sm lg:hidden",
|
|
158
158
|
aria_label: "Open menu") { render DocsUI::Icon.new("menu", class: "size-5") }
|
|
159
|
-
a(href: config.brand_href, class:
|
|
159
|
+
a(href: config.brand_href, class: topbar_brand_classes) { brand_mark }
|
|
160
160
|
app_home_link
|
|
161
161
|
end
|
|
162
162
|
render DocsUI::SearchBox.new if config.search_enabled?
|
|
@@ -169,6 +169,26 @@ module DocsUI
|
|
|
169
169
|
end
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
+
# The brand anchor's classes. config.topbar_brand = :mobile_only adds
|
|
173
|
+
# lg:hidden — at the drawer-pinned breakpoint the sidebar brand is already
|
|
174
|
+
# visible, so a site can drop the duplicate. The default (:always) keeps
|
|
175
|
+
# the pre-knob classes verbatim.
|
|
176
|
+
def topbar_brand_classes
|
|
177
|
+
base = "btn btn-ghost text-lg font-bold"
|
|
178
|
+
config.topbar_brand == :mobile_only ? "#{base} lg:hidden" : base
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The brand: the configured mark (config.brand_logo) when set, else the
|
|
182
|
+
# text brand — byte-identical to before for a site that sets nothing. The
|
|
183
|
+
# text brand stays the mark's accessible-name fallback.
|
|
184
|
+
def brand_mark
|
|
185
|
+
if (logo = config.brand_logo)
|
|
186
|
+
render DocsUI::Logo.new(logo, class: "h-6 w-auto", label: config.brand)
|
|
187
|
+
else
|
|
188
|
+
plain config.brand
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
172
192
|
# The opt-in App Home link (config.app_link) — the way back to the hosting
|
|
173
193
|
# app, rendered once, right after the brand. Nothing renders when unset, so
|
|
174
194
|
# the topbar stays byte-identical for a site that never configures it.
|
|
@@ -8,6 +8,11 @@ module DocsUI
|
|
|
8
8
|
#
|
|
9
9
|
# nav_groups is an ordered Hash:
|
|
10
10
|
# { "Heading" => { "Subgroup" => [DocsKit::NavItem, ...] } }
|
|
11
|
+
#
|
|
12
|
+
# Subgroups are the top level of the rendered menu. A lone heading (the common
|
|
13
|
+
# single-registry site) is not rendered at all — the brand masthead already
|
|
14
|
+
# labels the sidebar; with several headings, each renders as a STATIC
|
|
15
|
+
# `.menu-title` label (no fold) and only subgroups collapse.
|
|
11
16
|
class Sidebar < Phlex::HTML
|
|
12
17
|
include Phlex::Rails::Helpers::Request
|
|
13
18
|
include DaisyUI
|
|
@@ -29,7 +34,8 @@ module DocsUI
|
|
|
29
34
|
header_section
|
|
30
35
|
div(class: "flex-1 overflow-y-auto px-2 pb-6") do
|
|
31
36
|
Menu(class: "w-full gap-1") do
|
|
32
|
-
nav_groups.
|
|
37
|
+
groups = nav_groups.reject { |_, grouped| grouped.nil? || grouped.empty? }
|
|
38
|
+
groups.each { |heading, grouped| nav_group(heading, grouped, labeled: groups.size > 1) }
|
|
33
39
|
end
|
|
34
40
|
end
|
|
35
41
|
end
|
|
@@ -42,27 +48,37 @@ module DocsUI
|
|
|
42
48
|
|
|
43
49
|
def header_section
|
|
44
50
|
div(class: "flex min-h-16 items-center gap-2 px-4") do
|
|
45
|
-
a(href: config.brand_href, class: "text-lg font-bold text-base-content") {
|
|
51
|
+
a(href: config.brand_href, class: "text-lg font-bold text-base-content") { brand_mark }
|
|
46
52
|
badge = config.version_badge_text
|
|
47
53
|
span(class: "badge badge-sm badge-ghost") { badge } if badge
|
|
48
54
|
end
|
|
49
55
|
end
|
|
50
56
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
# The brand: the configured mark (config.brand_logo) when set, else the
|
|
58
|
+
# text brand — byte-identical to before for a site that sets nothing.
|
|
59
|
+
# Slightly taller than the topbar's h-6: this is the masthead.
|
|
60
|
+
def brand_mark
|
|
61
|
+
if (logo = config.brand_logo)
|
|
62
|
+
render DocsUI::Logo.new(logo, class: "h-7 w-auto", label: config.brand)
|
|
63
|
+
else
|
|
64
|
+
plain config.brand
|
|
65
|
+
end
|
|
66
|
+
end
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
# A top-level group (e.g. "Docs") holding collapsible sub-groups (e.g.
|
|
69
|
+
# "Guide", "Examples"). `grouped` is a { subgroup => [items] } Hash. The
|
|
70
|
+
# heading label only renders when the sidebar shows SEVERAL groups
|
|
71
|
+
# (labeled:) — as a static `.menu-title`, never a <details>, so subgroups
|
|
72
|
+
# stay at the menu's top level instead of gaining a nesting indent. The
|
|
73
|
+
# mt-4/first:mt-0 pair is the breathing room between one group's links and
|
|
74
|
+
# the next group's label.
|
|
75
|
+
def nav_group(heading, grouped, labeled:)
|
|
76
|
+
if labeled
|
|
77
|
+
li(class: "menu-title mt-4 text-xs font-semibold uppercase tracking-wider text-base-content/50 first:mt-0") do
|
|
78
|
+
heading
|
|
64
79
|
end
|
|
65
80
|
end
|
|
81
|
+
grouped.each { |subgroup, items| nav_subgroup(subgroup, items) }
|
|
66
82
|
end
|
|
67
83
|
|
|
68
84
|
# A collapsible sub-group: its title is a <summary> so the whole section folds
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsKit
|
|
4
|
+
# The normalized brand mark for the shell chrome (config.brand_logo) and the
|
|
5
|
+
# landing hero (config.landing.logo). A site configures a Hash in exactly one
|
|
6
|
+
# of five forms; DocsUI::Logo renders the result:
|
|
7
|
+
#
|
|
8
|
+
# { svg: "M0 0Z", viewbox: "0 0 24 24", label: "Acme" } # one path-d (landing-compat)
|
|
9
|
+
# { paths: ["M0 0Z", "M4 4Z"], viewbox: "…", label: "…" } # multi-path wordmark
|
|
10
|
+
# { markup: "<svg …>…</svg>", label: "Acme" } # raw SVG markup, embedded verbatim
|
|
11
|
+
# { file: "app/assets/images/mark.svg", label: "Acme" } # a .svg file, embedded inline
|
|
12
|
+
# { src: "logo.png", alt: "Acme" } # an <img> (not theme-adaptive)
|
|
13
|
+
#
|
|
14
|
+
# The svg:/paths: forms render each `d` as an ordinary Phlex-escaped attribute.
|
|
15
|
+
# The markup:/file: forms embed SITE-AUTHORED markup verbatim (see DocsUI::Logo
|
|
16
|
+
# for the trust rationale); both are shape-checked here — the content must be an
|
|
17
|
+
# <svg> element — so a mis-pasted snippet fails loudly at config time, never as
|
|
18
|
+
# a silently broken (or script-bearing) header. Mixing forms, or giving none,
|
|
19
|
+
# is ambiguous config and raises. `label`/`alt` fall back to each other so
|
|
20
|
+
# either knob names the mark for assistive tech.
|
|
21
|
+
#
|
|
22
|
+
# A file: mark memoizes its content and re-reads on an mtime change (the
|
|
23
|
+
# Configuration#openapi_document posture), so editing the SVG in development
|
|
24
|
+
# shows up without a server restart.
|
|
25
|
+
class BrandLogo
|
|
26
|
+
# The config keys that each select a render form — exactly one must be given.
|
|
27
|
+
FORM_KEYS = %i[svg paths markup file src].freeze
|
|
28
|
+
|
|
29
|
+
# A loose "is this an <svg> element" shape check for the markup:/file: forms.
|
|
30
|
+
SVG_SHAPE = /\A\s*<svg[\s>]/i
|
|
31
|
+
|
|
32
|
+
DEFAULT_VIEWBOX = "0 0 24 24"
|
|
33
|
+
|
|
34
|
+
attr_reader :paths, :viewbox, :markup, :file, :src
|
|
35
|
+
|
|
36
|
+
# Coerce a config value (Hash with symbol or string keys, or an
|
|
37
|
+
# already-normalized BrandLogo) into a BrandLogo.
|
|
38
|
+
def self.from(logo)
|
|
39
|
+
return logo if logo.is_a?(self)
|
|
40
|
+
|
|
41
|
+
new(logo.to_h)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(attrs = {})
|
|
45
|
+
attrs = attrs.transform_keys(&:to_sym)
|
|
46
|
+
given = attrs.slice(*FORM_KEYS).compact
|
|
47
|
+
unless given.size == 1
|
|
48
|
+
raise ArgumentError,
|
|
49
|
+
"brand_logo takes exactly one of #{FORM_KEYS.inspect} (got #{given.keys.inspect})"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@viewbox = attrs[:viewbox] || DEFAULT_VIEWBOX
|
|
53
|
+
@label = attrs[:label]
|
|
54
|
+
@alt = attrs[:alt]
|
|
55
|
+
build_form(given.keys.first, attrs)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The single path-d, for the landing-compat svg: shape (first of #paths).
|
|
59
|
+
def svg = paths&.first
|
|
60
|
+
|
|
61
|
+
def inline? = !paths.nil?
|
|
62
|
+
def markup? = !markup.nil?
|
|
63
|
+
def file? = !file.nil?
|
|
64
|
+
def image? = !src.nil?
|
|
65
|
+
|
|
66
|
+
# Whether the mark embeds site-authored markup verbatim (markup: or file:).
|
|
67
|
+
def embed? = markup? || file?
|
|
68
|
+
|
|
69
|
+
# The accessible name — label falls back to alt (and vice versa) so a site
|
|
70
|
+
# setting either names the mark; nil defers to the render-time brand fallback.
|
|
71
|
+
def label = @label || @alt
|
|
72
|
+
def alt = @alt || @label
|
|
73
|
+
|
|
74
|
+
# The markup to embed: the literal markup: string, or the file's content —
|
|
75
|
+
# memoized per mtime so a dev edit re-reads without a restart.
|
|
76
|
+
def svg_markup
|
|
77
|
+
return @markup if markup?
|
|
78
|
+
|
|
79
|
+
mtime = begin
|
|
80
|
+
@file.mtime
|
|
81
|
+
rescue StandardError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
return @file_content if defined?(@file_content) && @file_mtime == mtime
|
|
85
|
+
|
|
86
|
+
@file_mtime = mtime
|
|
87
|
+
@file_content = check_svg_shape!(@file.read, "file #{@file}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# Store the one given form. file: primes #svg_markup immediately so a bad
|
|
93
|
+
# file fails at config time (boot), not on first render.
|
|
94
|
+
def build_form(form, attrs)
|
|
95
|
+
case form
|
|
96
|
+
when :svg, :paths then @paths = Array(attrs[:paths] || attrs[:svg]).map(&:to_s)
|
|
97
|
+
when :markup then @markup = check_svg_shape!(attrs[:markup].to_s, "markup")
|
|
98
|
+
when :src then @src = attrs[:src]
|
|
99
|
+
when :file
|
|
100
|
+
@file = resolve_file!(attrs[:file])
|
|
101
|
+
svg_markup
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Validate + resolve the file: form eagerly, so a bad path fails at config
|
|
106
|
+
# time (boot), not on first render. Relative paths resolve against Rails.root
|
|
107
|
+
# when Rails is loaded, else the process working directory.
|
|
108
|
+
def resolve_file!(file)
|
|
109
|
+
path = Pathname.new(file.to_s)
|
|
110
|
+
path = Rails.root.join(path) if path.relative? && defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
111
|
+
raise ArgumentError, "brand_logo file must be a .svg (got #{path.basename})" unless path.extname.casecmp?(".svg")
|
|
112
|
+
raise ArgumentError, "brand_logo file not found: #{path}" unless path.file?
|
|
113
|
+
|
|
114
|
+
path
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The markup:/file: shape guard — the content must BE an <svg> element.
|
|
118
|
+
def check_svg_shape!(content, source)
|
|
119
|
+
return content if content.match?(SVG_SHAPE)
|
|
120
|
+
|
|
121
|
+
raise ArgumentError, "brand_logo #{source} must be an <svg> element (got #{content[0, 40].inspect})"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -193,6 +193,39 @@ module DocsKit
|
|
|
193
193
|
# sidebar, and page-masthead links). Read via #app_link, never @app_link.
|
|
194
194
|
attr_writer :app_link
|
|
195
195
|
|
|
196
|
+
# The opt-in shell brand mark, rendered by BOTH the topbar and the sidebar
|
|
197
|
+
# header in place of the text #brand (which stays the accessible-name
|
|
198
|
+
# fallback). A Hash in exactly one of the DocsKit::BrandLogo forms —
|
|
199
|
+
# svg:/paths: (inline path-d, theme-adaptive via currentColor), markup:/file:
|
|
200
|
+
# (site-authored <svg> embedded verbatim), or src: (an <img>, NOT
|
|
201
|
+
# theme-adaptive) — or an already-built BrandLogo. Defaults to nil → the
|
|
202
|
+
# text brand renders and the chrome is byte-identical to before. Sibling of
|
|
203
|
+
# c.landing.logo, which is the landing-hero mark. Read via #brand_logo,
|
|
204
|
+
# never @brand_logo.
|
|
205
|
+
def brand_logo=(value)
|
|
206
|
+
@brand_logo = nil
|
|
207
|
+
@brand_logo_raw = value
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Where the topbar renders the brand: :always (the default — byte-compat),
|
|
211
|
+
# or :mobile_only, which hides it at the drawer-pinned breakpoint (lg:)
|
|
212
|
+
# where the sidebar brand is already visible, deduplicating the mark.
|
|
213
|
+
attr_reader :topbar_brand
|
|
214
|
+
|
|
215
|
+
# The topbar-brand placements. At lg: the sidebar (with its own brand) is
|
|
216
|
+
# pinned open, so :mobile_only drops the duplicate; :always keeps it.
|
|
217
|
+
TOPBAR_BRAND_MODES = %i[always mobile_only].freeze
|
|
218
|
+
|
|
219
|
+
def topbar_brand=(value)
|
|
220
|
+
mode = value.respond_to?(:to_sym) ? value.to_sym : value
|
|
221
|
+
unless TOPBAR_BRAND_MODES.include?(mode)
|
|
222
|
+
raise ArgumentError,
|
|
223
|
+
"topbar_brand must be one of #{TOPBAR_BRAND_MODES.inspect} (got #{value.inspect})"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
@topbar_brand = mode
|
|
227
|
+
end
|
|
228
|
+
|
|
196
229
|
# External links rendered in the topbar next to the theme switcher — a repo
|
|
197
230
|
# link, a chat invite, a social profile. Each entry is a Hash
|
|
198
231
|
# ({ href:, label:, icon: }) or a DocsKit::TopbarLink; #topbar_links
|
|
@@ -274,6 +307,9 @@ module DocsKit
|
|
|
274
307
|
@app_link = nil
|
|
275
308
|
@topbar_links = []
|
|
276
309
|
@openapi = nil
|
|
310
|
+
@brand_logo = nil
|
|
311
|
+
@brand_logo_raw = nil
|
|
312
|
+
@topbar_brand = :always
|
|
277
313
|
end
|
|
278
314
|
|
|
279
315
|
# The normalized App Home link (a DocsKit::TopbarLink), or nil when unset —
|
|
@@ -284,6 +320,17 @@ module DocsKit
|
|
|
284
320
|
DocsKit::TopbarLink.from(@app_link)
|
|
285
321
|
end
|
|
286
322
|
|
|
323
|
+
# The normalized shell brand mark (a DocsKit::BrandLogo), or nil when unset.
|
|
324
|
+
# Memoized (and invalidated on reassignment) — unlike #app_link's rebuild-
|
|
325
|
+
# per-read, because a file: mark shape-checks and reads its SVG on build;
|
|
326
|
+
# per-render re-normalization would repeat that IO. A malformed value raises
|
|
327
|
+
# here, on first read — loud, never a silently broken header.
|
|
328
|
+
def brand_logo
|
|
329
|
+
return if @brand_logo_raw.nil?
|
|
330
|
+
|
|
331
|
+
@brand_logo ||= DocsKit::BrandLogo.from(@brand_logo_raw)
|
|
332
|
+
end
|
|
333
|
+
|
|
287
334
|
# The normalized topbar links (DocsKit::TopbarLink list), in declaration
|
|
288
335
|
# order. Each configured Hash/TopbarLink is coerced via TopbarLink.from, so
|
|
289
336
|
# the Shell only ever sees value objects. Blank/nil config yields [].
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "brand_logo"
|
|
4
|
+
|
|
3
5
|
module DocsKit
|
|
4
6
|
# The per-site landing-page knobs, read by DocsUI::Landing to render a marketing
|
|
5
7
|
# home page (hero + feature grid + doc index) without a site hand-rolling one.
|
|
@@ -90,11 +92,11 @@ module DocsKit
|
|
|
90
92
|
{ code: attrs[:code].to_s, filename: attrs[:filename], lexer: (attrs[:lexer] || :shell).to_sym }
|
|
91
93
|
end
|
|
92
94
|
|
|
93
|
-
# The hero logo as a normalized
|
|
95
|
+
# The hero logo as a normalized DocsKit::BrandLogo, or nil when unset.
|
|
94
96
|
def hero_logo
|
|
95
97
|
return if @logo.nil?
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
DocsKit::BrandLogo.from(@logo)
|
|
98
100
|
end
|
|
99
101
|
|
|
100
102
|
# One hero call-to-action button. `style` maps to a daisyUI btn variant
|
|
@@ -134,27 +136,9 @@ module DocsKit
|
|
|
134
136
|
end
|
|
135
137
|
end
|
|
136
138
|
|
|
137
|
-
# The hero brand logo
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
|
|
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
|
|
139
|
+
# The hero brand logo shape now lives in DocsKit::BrandLogo (shared with the
|
|
140
|
+
# shell's config.brand_logo); the old nested name stays as an alias so any
|
|
141
|
+
# site referencing LandingConfig::Logo keeps working.
|
|
142
|
+
Logo = DocsKit::BrandLogo
|
|
159
143
|
end
|
|
160
144
|
end
|
data/lib/docs_kit/version.rb
CHANGED
data/lib/docs_kit.rb
CHANGED
|
@@ -58,6 +58,9 @@ loader.ignore(File.expand_path("docs_kit/configuration.rb", __dir__))
|
|
|
58
58
|
# ignore it here too or zeitwerk double-manages the constant.
|
|
59
59
|
loader.ignore(File.expand_path("docs_kit/seo_config.rb", __dir__))
|
|
60
60
|
loader.ignore(File.expand_path("docs_kit/landing_config.rb", __dir__))
|
|
61
|
+
# Required eagerly by landing_config.rb (the LandingConfig::Logo alias resolves
|
|
62
|
+
# it at require time, before this loader is set up), so ignore it here too.
|
|
63
|
+
loader.ignore(File.expand_path("docs_kit/brand_logo.rb", __dir__))
|
|
61
64
|
# Loaded ONLY by the host's docs_kit:og rake task (an explicit require), never at
|
|
62
65
|
# gem runtime — so its Rack/browser tooling is never pulled into a host that
|
|
63
66
|
# doesn't run the task. Ignore it so eager_load! doesn't require it.
|
|
@@ -21,6 +21,23 @@ Rails.application.config.to_prepare do
|
|
|
21
21
|
# docs live under a subpath:
|
|
22
22
|
# c.brand_href = "/docs"
|
|
23
23
|
|
|
24
|
+
# Your own mark in the topbar + sidebar header instead of the text brand
|
|
25
|
+
# (which stays the accessible name). Exactly ONE of these forms:
|
|
26
|
+
# paths: — inline path-d list; fills with currentColor, so the mark
|
|
27
|
+
# recolors with the active theme (also `svg:` for one path)
|
|
28
|
+
# markup: — a full <svg>…</svg> string, embedded verbatim (use
|
|
29
|
+
# fill="currentColor" in it to stay theme-adaptive)
|
|
30
|
+
# file: — a .svg under this app (e.g. "app/assets/images/mark.svg"),
|
|
31
|
+
# embedded inline at render — same currentColor advice
|
|
32
|
+
# src: — an image asset path/URL rendered as an <img>; note an <img>
|
|
33
|
+
# canNOT inherit currentColor, so it won't adapt to the theme
|
|
34
|
+
# c.brand_logo = { paths: ["M4 2h9l5 5…Z"], viewbox: "0 0 81 45", label: "<%= app_brand %>" }
|
|
35
|
+
|
|
36
|
+
# On desktop (lg:) the pinned sidebar already shows the brand, so the topbar
|
|
37
|
+
# copy is a duplicate. :mobile_only hides the topbar brand at lg:; the
|
|
38
|
+
# default (:always) keeps it everywhere.
|
|
39
|
+
# c.topbar_brand = :mobile_only
|
|
40
|
+
|
|
24
41
|
# Docs embedded in a bigger app? Add the way BACK to that app — a labeled
|
|
25
42
|
# link rendered once in the topbar, right after the brand. brand_href is the
|
|
26
43
|
# DOCS home (brand, sidebar, and the page masthead's "← Docs home" all
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: docs-kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -175,6 +175,7 @@ files:
|
|
|
175
175
|
- app/components/docs_ui/icon.rb
|
|
176
176
|
- app/components/docs_ui/json_response.rb
|
|
177
177
|
- app/components/docs_ui/landing.rb
|
|
178
|
+
- app/components/docs_ui/logo.rb
|
|
178
179
|
- app/components/docs_ui/markdown.rb
|
|
179
180
|
- app/components/docs_ui/markdown_action.rb
|
|
180
181
|
- app/components/docs_ui/meta_tags.rb
|
|
@@ -205,6 +206,7 @@ files:
|
|
|
205
206
|
- lib/docs_kit/api_client.rb
|
|
206
207
|
- lib/docs_kit/api_request.rb
|
|
207
208
|
- lib/docs_kit/api_templates.rb
|
|
209
|
+
- lib/docs_kit/brand_logo.rb
|
|
208
210
|
- lib/docs_kit/configuration.rb
|
|
209
211
|
- lib/docs_kit/controller.rb
|
|
210
212
|
- lib/docs_kit/engine.rb
|