docs-kit 1.0.7 → 1.1.0

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -20
  3. data/app/components/docs_ui/archived_page.rb +45 -0
  4. data/app/components/docs_ui/brand_mark.rb +1 -2
  5. data/app/components/docs_ui/landing.rb +9 -18
  6. data/app/components/docs_ui/logo.rb +74 -0
  7. data/app/components/docs_ui/shell.rb +21 -1
  8. data/app/components/docs_ui/sidebar.rb +30 -14
  9. data/app/controllers/docs_kit/llms_controller.rb +8 -1
  10. data/app/controllers/docs_kit/mcp_controller.rb +5 -1
  11. data/app/controllers/docs_kit/search_controller.rb +6 -1
  12. data/exe/docs-kit +5 -5
  13. data/lib/docs_kit/brand_logo.rb +124 -0
  14. data/lib/docs_kit/configuration.rb +136 -4
  15. data/lib/docs_kit/controller.rb +11 -2
  16. data/lib/docs_kit/doc_version.rb +59 -0
  17. data/lib/docs_kit/landing_config.rb +8 -24
  18. data/lib/docs_kit/llms_text.rb +31 -4
  19. data/lib/docs_kit/markdown_export/blocks.rb +3 -2
  20. data/lib/docs_kit/mcp_tools.rb +2 -1
  21. data/lib/docs_kit/registry.rb +7 -0
  22. data/lib/docs_kit/scope.rb +59 -0
  23. data/lib/docs_kit/scoping.rb +28 -0
  24. data/lib/docs_kit/snapshot/entry.rb +48 -0
  25. data/lib/docs_kit/snapshot.rb +151 -0
  26. data/lib/docs_kit/templates/new_site.rb +64 -12
  27. data/lib/docs_kit/version.rb +1 -1
  28. data/lib/docs_kit.rb +3 -0
  29. data/lib/generators/docs_kit/install/install_generator.rb +1 -1
  30. data/lib/generators/docs_kit/install/templates/Dockerfile.tt +5 -5
  31. data/lib/generators/docs_kit/install/templates/agents_md.erb +1 -1
  32. data/lib/generators/docs_kit/install/templates/dockerignore +1 -0
  33. data/lib/generators/docs_kit/install/templates/docs_kit.rb.erb +17 -0
  34. data/lib/generators/docs_kit/install/templates/skill.md.erb +1 -1
  35. metadata +13 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa41c41824768d1db2a5ad5a7ba9b983b0f81032c8b5a3f80277e2611e203449
4
- data.tar.gz: 60cd31b54d83c9c6920ff48d27ec177f81b7a240fdf513e65f9617837cc6c738
3
+ metadata.gz: b53c8fcfcdc8d5053f9019ea56edc5a1b4fce1a8760a220eb19c76a297e72d53
4
+ data.tar.gz: 4289ad366b8462a9617db0e696250b717c21af1a30a97126367c370452156e1d
5
5
  SHA512:
6
- metadata.gz: e23f649705ac6216e4961c389eab64965961023123937364e4915b608264bf994ec0885c1958929884449d4eb4f6d9202ed6a5fa7fb9dd373c6a2202e8850ea1
7
- data.tar.gz: 0a047f0b8b45fdce21b9121b13461a5c003ec307586ef4b49bbb3ead0c4d79f44db00a243eda7792fc5be173f3b135195efad4212360527cb2ef5e143eafd5e5
6
+ metadata.gz: 80f49ed3f55338303fe09ebed720679c440eaadb02a99b112b312dfebafa70b081ccbf3622f20a7a0a6b6f48771969528746465377bda9961b62ce6cf9410521
7
+ data.tar.gz: 3dc20bc085a7baa55b0c64098931f03d00553b26e477a606ce1fe878850d96058992cd7ef3e4cbc59bc67c1ea07eb9be26219f2d1c5f7b83a04eb22e82737c47
data/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # docs-kit
2
2
 
3
- [![CI](https://github.com/mhenrixon/docs-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/mhenrixon/docs-kit/actions/workflows/ci.yml)
3
+ [![CI](https://github.com/zoolutions/docs-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/zoolutions/docs-kit/actions/workflows/ci.yml)
4
4
 
5
5
  Shared [Phlex](https://www.phlex.fun) chrome for documentation sites built on
6
6
  [daisyUI](https://daisyui.com). Extract the shell, sidebar, code blocks, theme
7
7
  switcher, and page kit into one gem so multiple docs sites look identical and are
8
8
  maintained in one place.
9
9
 
10
- Reactive demos ([phlex-reactive](https://github.com/mhenrixon/phlex-reactive))
11
- and Postgres-SSE transport ([pgbus](https://github.com/mhenrixon/pgbus)) are
10
+ Reactive demos ([phlex-reactive](https://github.com/zoolutions/phlex-reactive))
11
+ and Postgres-SSE transport ([pgbus](https://github.com/zoolutions/pgbus)) are
12
12
  **optional, runtime-detected** add-ons — docs-kit does not depend on them.
13
13
 
14
14
  ## What you get
@@ -137,7 +137,7 @@ fronts Puma with Thruster (`CMD ["./bin/thrust", "./bin/rails", "server"]`) for
137
137
  HTTP caching, compression, and X-Sendfile — and the generator scaffolds the
138
138
  `bin/thrust` binstub if the app lacks one, since the exec-form CMD needs the
139
139
  file to exist in the image. Thruster listens on the routed port
140
- (`HTTP_PORT=3000` — Kamal's `app_port`) and proxies to Puma on `TARGET_PORT=3001`.
140
+ (`HTTP_PORT=3000` — dash's `app_port`) and proxies to Puma on `TARGET_PORT=3001`.
141
141
  Without thruster in the *production* bundle (absent, or only in a
142
142
  development/test group that `BUNDLE_WITHOUT` excludes) the CMD falls back to
143
143
  plain `rails server` — never a thrust CMD that would crash at boot.
@@ -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
@@ -662,7 +701,7 @@ claude mcp add --transport http docs https://your-docs.example/mcp
662
701
 
663
702
  and can ask Claude to search or read your docs, which now appear as tools. The
664
703
  JSON-RPC is stateless (each `POST` is independent — no SSE session), so it works
665
- behind the existing Kamal/Cloudflare deploy unchanged; `GET`/`DELETE` return
704
+ behind the existing dash/Cloudflare deploy unchanged; `GET`/`DELETE` return
666
705
  `405`. When enabled, `/llms.txt` grows a final `## MCP` line advertising the
667
706
  endpoint so agents discover it.
668
707
 
@@ -805,7 +844,7 @@ back to a spec.
805
844
 
806
845
  ```bash
807
846
  docs-kit new my-docs # → a complete, deployable docs app
808
- docs-kit new my-docs --image mhenrixon/my-repo --service my-repo
847
+ docs-kit new my-docs --image zoolutions/my-repo --service my-repo
809
848
  ```
810
849
 
811
850
  `docs-kit new` runs `rails new` (propshaft + importmap + turbo/stimulus, no DB)
@@ -815,7 +854,7 @@ and applies docs-kit's application template, which:
815
854
  - runs `rails g docs_kit:install` (initializers, controllers, a Doc registry, a
816
855
  sample guide page, the Bun/Tailwind build, the docs-nav Stimulus wiring),
817
856
  - syncs the lucide icons and builds the CSS,
818
- - scaffolds Kamal (`config/deploy.yml`, `.kamal/secrets`, an optimized
857
+ - scaffolds dash (`config/deploy.yml`, `.dash/secrets`, an optimized
819
858
  multi-stage `Dockerfile` + a `.dockerignore`) and a thin
820
859
  `.github/workflows/deploy-docs.yml` that calls the reusable workflow.
821
860
 
@@ -866,7 +905,7 @@ group. Then `bundle exec rubocop` runs the docs-kit cops.
866
905
  The build + deploy is defined **once** in this gem's reusable workflow
867
906
  (`.github/workflows/deploy.yml`). `docs-kit new` scaffolds the caller for you; to
868
907
  wire it by hand a site adds five small things and it deploys to the
869
- oss-infrastructure server (Kamal + GHCR + Cloudflare Tunnel).
908
+ oss-infrastructure server (dash + GHCR + Cloudflare Tunnel).
870
909
 
871
910
  **1. A thin caller** — `.github/workflows/deploy-docs.yml`:
872
911
 
@@ -877,46 +916,73 @@ on:
877
916
  workflow_dispatch:
878
917
  jobs:
879
918
  deploy:
880
- uses: mhenrixon/docs-kit/.github/workflows/deploy.yml@main
919
+ uses: zoolutions/docs-kit/.github/workflows/deploy.yml@main
881
920
  with:
882
- image: mhenrixon/<repo> # OWNER/REPO — see naming note below
921
+ image: zoolutions/<repo> # OWNER/REPO — see naming note below
883
922
  service: <repo>
884
923
  secrets: inherit
885
924
  ```
886
925
 
887
- **2. `docs/config/deploy.yml`** — `service:` and `image:` MUST match the caller:
926
+ **2. `docs/config/deploy.yml`** — `service:` and `image:` MUST match the caller
927
+ (the full file `docs-kit new` writes is in `lib/docs_kit/templates/new_site.rb`;
928
+ `dash docs proxy` documents every key):
888
929
 
889
930
  ```yaml
890
931
  service: <repo>
891
- image: mhenrixon/<repo>
892
- registry: { server: ghcr.io, username: mhenrixon, password: [KAMAL_REGISTRY_PASSWORD] }
932
+ image: zoolutions/<repo>
933
+ minimum_version: 4.0.0 # dash 4: dash-proxy identity + in-place host migration
934
+ retain_containers: 2
935
+ error_pages_path: public # 502/503/504.html served during a deploy gap
936
+ registry: { server: ghcr.io, username: mhenrixon, password: [DASH_REGISTRY_PASSWORD] }
893
937
  builder: { arch: amd64, context: .., dockerfile: Dockerfile } # repo root = build context
894
- proxy: { host: <%= ENV["DEPLOY_DOMAIN"] %>, app_port: 3000, ssl: false, healthcheck: { path: /up } }
895
938
  servers: { web: { hosts: [<%= ENV["DEPLOY_HOST"] %>] } }
896
939
  ssh: { user: oss }
940
+ proxy:
941
+ host: <%= ENV["DEPLOY_DOMAIN"] %>
942
+ app_port: 3000
943
+ ssl: false # TLS terminates at Cloudflare
944
+ healthcheck: { path: /up, interval: 5, timeout: 30 }
945
+ compress: true # zstd/br/gzip at the edge
946
+ cache: { enabled: true, max_ttl: 300 } # stores `Cache-Control: public` responses (assets, /llms*.txt)
947
+ headers: { response: { set: { X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin }, remove: [Server, X-Powered-By] } }
948
+ intercept_errors: [502, 503, 504]
949
+ exclude_metrics_paths: [/up]
897
950
  ```
898
951
 
952
+ Deliberately **not** set: `proxy.run` (`port_holder`, `log_format`, …) is
953
+ host-wide — every docs site on the shared host boots the same proxy, and a
954
+ `run:` block that differs between them makes each alternate deploy reboot it.
955
+ `rate_limit`/`deny_ips` need `client_ip.trusted_proxies` pinned to the tunnel's
956
+ address to key on visitors rather than on cloudflared; add them per site once
957
+ that address is known.
958
+
899
959
  **3. `docs/Dockerfile`** — end the final stage with the matching label:
900
960
 
901
961
  ```dockerfile
902
962
  LABEL service="<repo>"
903
963
  ```
904
964
 
905
- **4. `docs/.kamal/secrets`** — `KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD`.
965
+ **4. `docs/.dash/secrets`** — `DASH_REGISTRY_PASSWORD=$DASH_REGISTRY_PASSWORD`.
906
966
 
907
967
  **5. GitHub** — a `docs` environment with secrets `SSH_PRIVATE_KEY`,
908
968
  `DEPLOY_HOST`, `DEPLOY_DOMAIN`. (The registry password is the auto-provided
909
969
  `GITHUB_TOKEN` — no PAT.)
910
970
 
911
971
  > **Naming — use the repo name, not `<repo>-docs`.** `image`/`service` must be
912
- > the calling repo's `OWNER/REPO`. Pushing `ghcr.io/mhenrixon/<repo>` from the
972
+ > the calling repo's `OWNER/REPO`. Pushing `ghcr.io/zoolutions/<repo>` from the
913
973
  > repo's own Actions run auto-links the package to the repo, so `GITHUB_TOKEN`
914
974
  > can both push (build job) and pull (deploy) it. A different name becomes an
915
975
  > unlinked user-scoped package `GITHUB_TOKEN` can't pull → the deploy fails.
916
976
 
917
- **First deploy per host:** run `kamal setup` (or `bin/deploy setup`) once to boot
918
- any accessories (e.g. a Postgres accessory); the release workflow runs plain
919
- `kamal deploy`, which doesn't boot accessories.
977
+ **First deploy per host:** run `dash setup` (or `bin/deploy setup`) once to boot
978
+ any accessories (e.g. a Postgres accessory); the release workflow runs
979
+ `dash doctor` (a pre-flight of host, registry, proxy, ports and readiness gates)
980
+ and then plain `dash deploy`, which doesn't boot accessories.
981
+
982
+ **Upgrading a host to dash 4:** the first 4.x deploy renames the proxy
983
+ (`kamal-proxy` → `dash-proxy`, network `kamal` → `dash`, config volume copied)
984
+ and costs one short outage on that host while ports 80/443 change hands. It is
985
+ idempotent and shared by every site on the host — whichever deploys first pays it.
920
986
 
921
987
  ## CSS — the canonical build
922
988
 
@@ -1040,7 +1106,7 @@ them:
1040
1106
  → your profile → *Trusted Publishers* → *Create*, add a **pending** publisher
1041
1107
  (works for a gem not yet pushed) with:
1042
1108
  - Gem name: `docs-kit`
1043
- - Repository: `mhenrixon/docs-kit`
1109
+ - Repository: `zoolutions/docs-kit`
1044
1110
  - Workflow filename: `release.yml`
1045
1111
  - Environment: `rubygems`
1046
1112
  2. **GitHub `rubygems` environment.** Repo *Settings → Environments → New
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocsUI
4
+ # Renders one page of an ARCHIVED documentation version — a frozen Markdown
5
+ # body (DocsKit::Snapshot::Entry) through today's live chrome, so archived
6
+ # docs get every future Shell/Sidebar/Code fix for free. The live counterpart
7
+ # is DocsUI::Page; this mirrors its shape with the content coming from the
8
+ # snapshot file instead of an authored #content method.
9
+ #
10
+ # Every kwarg defaults, so even a naive `entry.view_class.new` (a custom
11
+ # registry predating #renderable) renders an empty page rather than raising.
12
+ #
13
+ # NOTE (issue #61 phase 4): the "you are viewing the 1.0 docs" banner with a
14
+ # link to the current equivalent lands with the version switcher, not here.
15
+ #
16
+ # Deliberately does NOT include Phlex::Rails::Helpers::Routes/Request — their
17
+ # bodies run Rails.* at class load, which would make this class (and
18
+ # everything referencing it, like Snapshot::Entry#view_class) unloadable in a
19
+ # Rails-free render. Nothing here needs a request.
20
+ class ArchivedPage < Phlex::HTML
21
+ include DocsUI
22
+
23
+ def initialize(entry: nil)
24
+ @entry = entry
25
+ end
26
+
27
+ def view_template
28
+ render DocsUI::Shell.new(title: @entry&.title) { body }
29
+ end
30
+
31
+ # The masthead + Markdown body — separated from the Shell wrapper so it can
32
+ # render (and be specced) without a Rails view context, the same seam as
33
+ # Shell's own topbar/theme-script specs.
34
+ def body
35
+ render DocsUI::Header.new(@entry.title) if @entry&.title
36
+ render DocsUI::Markdown.new(markdown_source) unless markdown_source.empty?
37
+ end
38
+
39
+ private
40
+
41
+ def markdown_source
42
+ @markdown_source ||= @entry ? @entry.markdown.to_s : ""
43
+ end
44
+ end
45
+ end
@@ -19,7 +19,7 @@ module DocsUI
19
19
  # token => official Simple-Icons path data (viewBox 0 0 24 24, fill rule
20
20
  # nonzero). Sourced verbatim from simpleicons.org; each is a single <path d>.
21
21
  # The path strings are inherently one long line each — never reflow them.
22
- # rubocop:disable Layout/LineLength
22
+ # rubocop:disable-next Layout/LineLength
23
23
  BRANDS = {
24
24
  github: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12",
25
25
  gitlab: "m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z",
@@ -36,7 +36,6 @@ module DocsUI
36
36
  reddit: "M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z",
37
37
  stackoverflow: "M15.725 0l-1.72 1.277 6.39 8.588 1.716-1.277L15.725 0zm-3.94 3.418l-1.369 1.644 8.225 6.85 1.369-1.644-8.225-6.85zm-3.15 4.465l-.905 1.94 9.702 4.517.904-1.94-9.701-4.517zm-1.85 4.86l-.44 2.093 10.473 2.201.44-2.092-10.473-2.203zM1.89 15.47V24h19.19v-8.53h-2.133v6.397H4.021v-6.396H1.89zm4.265 2.133v2.13h10.66v-2.13H6.154Z"
38
38
  }.freeze
39
- # rubocop:enable Layout/LineLength
40
39
 
41
40
  # True when `token` names a shipped brand mark (symbol or string), false for a
42
41
  # lucide name, nil, or a blank string. The Shell uses this to decide whether a
@@ -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 — an inline single-path SVG (currentColor, theme-adaptive) or
67
- # an <img>. Rendered above the eyebrow, like a product wordmark.
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
- if mark.inline?
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
- groups.each { |heading, items| doc_index_group(heading, items) }
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: "btn btn-ghost text-lg font-bold") { config.brand }
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.each { |heading, grouped| nav_group(heading, grouped) }
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") { config.brand }
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
- # A top-level collapsible group (e.g. "Docs") holding collapsible sub-groups
52
- # (e.g. "Guide", "Examples"). `grouped` is a { subgroup => [items] } Hash.
53
- def nav_group(heading, grouped)
54
- return if grouped.nil? || grouped.empty?
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
- li do
57
- details(open: true) do
58
- summary(class: "text-xs font-semibold uppercase tracking-wider text-base-content/50 #{MARKER_RESET}") do
59
- heading
60
- end
61
- ul do
62
- grouped.each { |subgroup, items| nav_subgroup(subgroup, items) }
63
- end
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
@@ -29,6 +29,11 @@ module DocsKit
29
29
  # GET-only, sessionless, public text endpoints (no token to verify).
30
30
  protect_from_forgery with: :null_session
31
31
 
32
+ # Every action runs in the request's version scope (params[:version] on the
33
+ # version-prefixed routes, else the current version), so the enumeration
34
+ # below serves the version the URL asked for.
35
+ include DocsKit::Scoping
36
+
32
37
  def index
33
38
  body = DocsKit::LlmsText.index(docs_config, base_url: request.base_url)
34
39
  render_text(body) if stale_llms?(body)
@@ -67,9 +72,11 @@ module DocsKit
67
72
  # A page's Markdown twin, rendered through this controller's view context so
68
73
  # url helpers/CSRF resolve and relative links absolutize to portable URLs —
69
74
  # the same path DocsKit::Controller#render_page takes for a `.md` request.
75
+ # renderable_for is the live-or-snapshot shim (a snapshot entry renders an
76
+ # ArchivedPage carrying its frozen Markdown).
70
77
  def render_page_markdown(page)
71
78
  DocsKit::MarkdownExport.new(
72
- page.view_class.new, view_context:, base_url: request.base_url
79
+ DocsKit::LlmsText.renderable_for(page), view_context:, base_url: request.base_url
73
80
  ).to_md
74
81
  end
75
82
  end
@@ -15,7 +15,7 @@ module DocsKit
15
15
  # DocsKit::McpServer / DocsKit::McpTools.
16
16
  #
17
17
  # Stateless JSON-RPC: each POST is independent (no SSE session), so it works
18
- # behind the existing Kamal/Cloudflare deploy unchanged. #create delegates the
18
+ # behind the existing dash/Cloudflare deploy unchanged. #create delegates the
19
19
  # whole protocol to DocsKit::McpServer#handle_json — the SDK parses the request,
20
20
  # dispatches the tool, and serializes the response (including JSON-RPC errors),
21
21
  # so the controller never hand-rolls the protocol.
@@ -31,6 +31,10 @@ module DocsKit
31
31
  # protection outright.
32
32
  skip_forgery_protection
33
33
 
34
+ # MCP tool calls run in the request's version scope; the version-aware tool
35
+ # arguments (issue #61 phase 6) layer per-call resolution on top of this.
36
+ include DocsKit::Scoping
37
+
34
38
  def create
35
39
  return head(:not_found) unless docs_config.mcp_enabled?
36
40
 
@@ -28,6 +28,11 @@ module DocsKit
28
28
  # public endpoint.
29
29
  protect_from_forgery with: :null_session
30
30
 
31
+ # Search follows the request's version: /1.0/docs/search searches the 1.0
32
+ # snapshot, /docs/search searches current — the scope swaps the enumeration
33
+ # source underneath DocsKit::LlmsText.pages.
34
+ include DocsKit::Scoping
35
+
31
36
  def index
32
37
  hits = search_index.search(query)
33
38
 
@@ -53,7 +58,7 @@ module DocsKit
53
58
  def search_index
54
59
  triples = DocsKit::LlmsText.pages(docs_config).map do |page|
55
60
  markdown = DocsKit::MarkdownExport.new(
56
- page.view_class.new, view_context:, base_url: request.base_url
61
+ DocsKit::LlmsText.renderable_for(page), view_context:, base_url: request.base_url
57
62
  ).to_md
58
63
  [page.title, page.href, markdown]
59
64
  end
data/exe/docs-kit CHANGED
@@ -4,11 +4,11 @@
4
4
  # docs-kit CLI. One command scaffolds a complete, deployable docs site:
5
5
  #
6
6
  # docs-kit new my-docs
7
- # docs-kit new my-docs --image mhenrixon/my-repo --service my-repo
7
+ # docs-kit new my-docs --image zoolutions/my-repo --service my-repo
8
8
  #
9
9
  # It runs `rails new` with the right minimal flags and applies docs-kit's
10
10
  # application template (lib/docs_kit/templates/new_site.rb), which adds the gem,
11
- # runs `docs_kit:install`, syncs icons, builds the CSS, and scaffolds Kamal +
11
+ # runs `docs_kit:install`, syncs icons, builds the CSS, and scaffolds dash +
12
12
  # the reusable deploy workflow.
13
13
 
14
14
  require "optparse"
@@ -25,9 +25,9 @@ unless command == "new"
25
25
  docs-kit new NAME [--image OWNER/REPO] [--service NAME] [--gem-source SRC]
26
26
 
27
27
  Options:
28
- --image GHCR image (default mhenrixon/NAME) — use OWNER/REPO for the
28
+ --image GHCR image (default zoolutions/NAME) — use OWNER/REPO for the
29
29
  repo-linked package so GITHUB_TOKEN can push+pull it.
30
- --service Kamal service name (default NAME).
30
+ --service dash service name (default NAME).
31
31
  --gem-source How to depend on docs-kit: 'released' (default),
32
32
  'path:PATH', or 'github:OWNER/REPO'.
33
33
  USAGE
@@ -58,7 +58,7 @@ gem_source =
58
58
 
59
59
  env = {
60
60
  "DOCS_KIT_GEM_SOURCE" => gem_source,
61
- "DOCS_KIT_IMAGE" => opts[:image] || "mhenrixon/#{name}",
61
+ "DOCS_KIT_IMAGE" => opts[:image] || "zoolutions/#{name}",
62
62
  "DOCS_KIT_SERVICE" => opts[:service] || name
63
63
  }
64
64