markdowndocs 0.9.0 → 0.10.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.
- checksums.yaml +4 -4
- data/.tool-versions +1 -0
- data/CHANGELOG.md +31 -0
- data/app/assets/javascripts/markdowndocs/controllers/docs_mode_controller.js +34 -0
- data/app/controllers/markdowndocs/docs_controller.rb +2 -1
- data/app/controllers/markdowndocs/preferences_controller.rb +33 -4
- data/app/views/layouts/markdowndocs/application.html.erb +3 -0
- data/app/views/markdowndocs/docs/_docs_toolbar.html.erb +10 -0
- data/app/views/markdowndocs/docs/_mode_switcher.html.erb +14 -65
- data/app/views/markdowndocs/docs/_navigation.html.erb +1 -7
- data/app/views/markdowndocs/docs/index.html.erb +3 -0
- data/app/views/markdowndocs/docs/show.html.erb +4 -4
- data/config/locales/en.yml +2 -0
- data/lib/markdowndocs/version.rb +1 -1
- data/mise.toml +2 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ca89d7b3d1decf28b07cfaccfd1e2e0e0924f8ef7d56d1374e3cb600cbd4933
|
|
4
|
+
data.tar.gz: 036b99af3190b230d1ceb327171300758b76d09392a4d28b32a9a0129b9cdaf3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80f8fb1ad69d4e83407943a1655a7da909d3704ce950ccb3e01a01a515aa3b7a96804e3ae93ceea7ed2cace9497267d750010a2c6fd05b56db27abc89f597432
|
|
7
|
+
data.tar.gz: 7b7a5ba65d0e5e549811734b03aed0bcc5d52b0c13b768f5e06599162b367998f7d4f7294fe7af2a37d82924ce1826e7e65fbfa39b616bf97828f6b68f83fe2a
|
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 3.4.5
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.10.0] - 2026-06-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Persistent audience switcher.** The viewing-mode switcher now renders in a
|
|
13
|
+
toolbar at the top of both the docs index and every doc page (new
|
|
14
|
+
`markdowndocs/docs/_docs_toolbar` partial), instead of only the show-page
|
|
15
|
+
sidebar — the audience choice is always visible and rendered once per page.
|
|
16
|
+
`DocsController#index` and `#show` both expose `@available_modes` from
|
|
17
|
+
`config.modes`.
|
|
18
|
+
- **Screen-reader mode announcement.** Switching audience sets `flash[:notice]`
|
|
19
|
+
(`markdowndocs.mode_announcement`), surfaced in a `role="status"
|
|
20
|
+
aria-live="polite"` region in the engine layout so the Turbo-replace mode
|
|
21
|
+
change isn't silent for assistive tech. Hosts may render the flash their own way.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **No-counterpart audience switch goes to the target index.** Switching to a
|
|
26
|
+
mode where the current doc has no counterpart (and no shared-root fallback)
|
|
27
|
+
now redirects to that audience's index — with a distinct flash
|
|
28
|
+
(`markdowndocs.no_counterpart_announcement`) — instead of stranding the reader
|
|
29
|
+
on a doc outside the audience they chose. Topic-preserving (scoped counterpart)
|
|
30
|
+
and shared-root docs are unchanged.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Mode switcher ARIA: toggle-button group, not a radiogroup.** Each mode is a
|
|
35
|
+
submit button using `aria-pressed` (was `role="radio"` / `aria-checked`, which
|
|
36
|
+
implied unimplemented arrow-key navigation). Focus returns to the chosen button
|
|
37
|
+
after the Turbo replace.
|
|
38
|
+
|
|
8
39
|
## [0.9.0] - 2026-06-21
|
|
9
40
|
|
|
10
41
|
### Added
|
|
@@ -16,11 +16,45 @@ export default class extends Controller {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
static STORAGE_KEY = "markdowndocs_mode"
|
|
19
|
+
static FOCUS_KEY = "markdowndocs_focus_mode"
|
|
19
20
|
|
|
20
21
|
connect() {
|
|
21
22
|
if (!this.isAuthenticated()) {
|
|
22
23
|
this.restoreGuestMode()
|
|
23
24
|
}
|
|
25
|
+
this.restoreFocusAfterToggle()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
rememberFocus(event) {
|
|
29
|
+
// Save the mode the user just pressed so we can restore focus to the
|
|
30
|
+
// matching button after Turbo replaces the page (a11y: keep focus on
|
|
31
|
+
// the control the user actuated, not on the article wrapper).
|
|
32
|
+
try {
|
|
33
|
+
const mode = event.currentTarget.dataset.mode
|
|
34
|
+
if (mode) {
|
|
35
|
+
sessionStorage.setItem(this.constructor.FOCUS_KEY, mode)
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// sessionStorage unavailable — accept the focus loss gracefully.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
restoreFocusAfterToggle() {
|
|
43
|
+
let pendingMode
|
|
44
|
+
try {
|
|
45
|
+
pendingMode = sessionStorage.getItem(this.constructor.FOCUS_KEY)
|
|
46
|
+
if (pendingMode) sessionStorage.removeItem(this.constructor.FOCUS_KEY)
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
if (!pendingMode) return
|
|
51
|
+
|
|
52
|
+
const button = this.element.querySelector(`button[data-mode="${pendingMode}"]`)
|
|
53
|
+
if (button) {
|
|
54
|
+
// Defer past the Turbo render + autofocus on the article wrapper so
|
|
55
|
+
// we steal focus from it; otherwise autofocus would win the race.
|
|
56
|
+
window.requestAnimationFrame(() => button.focus())
|
|
57
|
+
}
|
|
24
58
|
}
|
|
25
59
|
|
|
26
60
|
isAuthenticated() {
|
|
@@ -14,6 +14,7 @@ module Markdowndocs
|
|
|
14
14
|
# surviving docs are dropped (see Documentation.grouped_by_category).
|
|
15
15
|
@docs_by_category = Documentation.grouped_by_category(mode: @docs_mode)
|
|
16
16
|
@search_enabled = Markdowndocs.config.search_enabled
|
|
17
|
+
@available_modes = Markdowndocs.config.modes
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def search_index
|
|
@@ -64,7 +65,7 @@ module Markdowndocs
|
|
|
64
65
|
)
|
|
65
66
|
@rendered_content = helpers.add_heading_anchors(rendered_html)
|
|
66
67
|
@related_docs = Documentation.by_category(@doc.category).reject { |d| d.path_slug == @doc.path_slug }
|
|
67
|
-
@available_modes =
|
|
68
|
+
@available_modes = Markdowndocs.config.modes
|
|
68
69
|
@toc_items = helpers.generate_table_of_contents(@rendered_content)
|
|
69
70
|
end
|
|
70
71
|
|
|
@@ -25,7 +25,25 @@ module Markdowndocs
|
|
|
25
25
|
httponly: true
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
current_path = params[:current_path]
|
|
29
|
+
target = smart_nav_target(mode, current_path)
|
|
30
|
+
mode_name = I18n.t("markdowndocs.modes.#{mode}", default: mode.titleize)
|
|
31
|
+
|
|
32
|
+
flash[:notice] = if redirected_to_index_from_doc?(target, current_path)
|
|
33
|
+
I18n.t(
|
|
34
|
+
"markdowndocs.no_counterpart_announcement",
|
|
35
|
+
mode: mode_name,
|
|
36
|
+
default: "No %{mode} version of this page — showing the %{mode} documentation."
|
|
37
|
+
)
|
|
38
|
+
else
|
|
39
|
+
I18n.t(
|
|
40
|
+
"markdowndocs.mode_announcement",
|
|
41
|
+
mode: mode_name,
|
|
42
|
+
default: "Now viewing %{mode}."
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
redirect_to(target, status: :see_other)
|
|
29
47
|
end
|
|
30
48
|
|
|
31
49
|
private
|
|
@@ -50,15 +68,26 @@ module Markdowndocs
|
|
|
50
68
|
# rejection (and any other reachability rules) match what the show
|
|
51
69
|
# action would actually serve. Bypassing this — e.g. with raw
|
|
52
70
|
# File.exist? — can redirect the user into a 404.
|
|
53
|
-
if scoped_sibling_reachable?(slug, target_mode)
|
|
71
|
+
if scoped_sibling_reachable?(slug, target_mode)
|
|
54
72
|
scoped_url
|
|
55
|
-
elsif root_sibling_reachable?(slug)
|
|
73
|
+
elsif root_sibling_reachable?(slug)
|
|
56
74
|
root_url
|
|
57
75
|
else
|
|
58
|
-
|
|
76
|
+
# No counterpart in the target mode and no shared root fallback: send the
|
|
77
|
+
# reader to that audience's index rather than stranding them on a doc
|
|
78
|
+
# outside the audience they just chose.
|
|
79
|
+
index_path
|
|
59
80
|
end
|
|
60
81
|
end
|
|
61
82
|
|
|
83
|
+
# True when the switch had to fall back to the docs index because the current
|
|
84
|
+
# doc has no counterpart in the target mode (selects the flash copy below).
|
|
85
|
+
def redirected_to_index_from_doc?(target, current_path)
|
|
86
|
+
current_path.present? &&
|
|
87
|
+
target == markdowndocs.root_path.chomp("/") &&
|
|
88
|
+
extract_slug_from_path(current_path).present?
|
|
89
|
+
end
|
|
90
|
+
|
|
62
91
|
def scoped_sibling_reachable?(slug, target_mode)
|
|
63
92
|
docs_path = Markdowndocs.config.resolved_docs_path
|
|
64
93
|
scoped_file = docs_path.join(target_mode, "#{slug}.md")
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
</head>
|
|
12
12
|
|
|
13
13
|
<body class="min-h-screen flex flex-col bg-gray-50">
|
|
14
|
+
<% if flash[:notice].present? %>
|
|
15
|
+
<div role="status" aria-live="polite" class="sr-only"><%= flash[:notice] %></div>
|
|
16
|
+
<% end %>
|
|
14
17
|
<%= yield :docs_header %>
|
|
15
18
|
|
|
16
19
|
<main id="main-content" role="main" class="flex-1">
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%# locals: (current_mode:, available_modes:) %>
|
|
2
|
+
<%# Persistent docs toolbar: the audience switcher, shown at the top of both
|
|
3
|
+
the index and every doc page. Renders nothing when only one mode exists. %>
|
|
4
|
+
<% if available_modes.length > 1 %>
|
|
5
|
+
<div class="flex justify-end mb-6">
|
|
6
|
+
<%= render "markdowndocs/docs/mode_switcher",
|
|
7
|
+
current_mode: current_mode,
|
|
8
|
+
available_modes: available_modes %>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
@@ -1,80 +1,29 @@
|
|
|
1
1
|
<%# locals: (current_mode:, available_modes:) %>
|
|
2
|
-
<%#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
can appear N times without colliding. Tests / selectors should target
|
|
7
|
-
[data-controller="docs-mode"] rather than #docs-mode-switcher. %>
|
|
2
|
+
<%# Horizontal audience toggle for the docs toolbar (rendered once per page).
|
|
3
|
+
Toggle-button group, NOT a radiogroup: each option is its own form +
|
|
4
|
+
submit button, so aria-pressed describes state. No element id — tests
|
|
5
|
+
target [data-controller="docs-mode"]. %>
|
|
8
6
|
<div
|
|
9
|
-
class="
|
|
10
|
-
bg-white
|
|
11
|
-
dark:bg-slate-800
|
|
12
|
-
rounded-lg
|
|
13
|
-
border
|
|
14
|
-
border-slate-200
|
|
15
|
-
dark:border-slate-700
|
|
16
|
-
p-4
|
|
17
|
-
"
|
|
7
|
+
class="flex items-center gap-2"
|
|
18
8
|
data-controller="docs-mode"
|
|
19
9
|
data-docs-mode-current-value="<%= current_mode %>"
|
|
20
10
|
>
|
|
21
|
-
<
|
|
22
|
-
text-sm
|
|
23
|
-
font-medium
|
|
24
|
-
text-slate-700
|
|
25
|
-
dark:text-slate-300
|
|
26
|
-
mb-3
|
|
27
|
-
">
|
|
11
|
+
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
28
12
|
<%= t("markdowndocs.viewing_mode") %>
|
|
29
|
-
</
|
|
30
|
-
|
|
31
|
-
<div
|
|
32
|
-
class="space-y-2"
|
|
33
|
-
role="radiogroup"
|
|
34
|
-
aria-label="<%= t('markdowndocs.select_viewing_mode') %>"
|
|
35
|
-
>
|
|
13
|
+
</span>
|
|
14
|
+
<div class="inline-flex gap-1" role="group" aria-label="<%= t('markdowndocs.select_viewing_mode') %>">
|
|
36
15
|
<% available_modes.each do |mode| %>
|
|
37
|
-
<%= form_with(
|
|
38
|
-
url: markdowndocs.preference_path,
|
|
39
|
-
method: :patch,
|
|
40
|
-
data: {
|
|
41
|
-
turbo_action: "replace"
|
|
42
|
-
}
|
|
43
|
-
) do |f| %>
|
|
16
|
+
<%= form_with(url: markdowndocs.preference_path, method: :patch, data: {turbo_action: "replace"}) do |f| %>
|
|
44
17
|
<%= f.hidden_field :mode, value: mode, id: nil %>
|
|
45
18
|
<%= f.hidden_field :current_path, value: request.fullpath, id: nil %>
|
|
46
19
|
<button
|
|
47
20
|
type="submit"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
text-left
|
|
53
|
-
p-3
|
|
54
|
-
rounded-lg
|
|
55
|
-
border-2
|
|
56
|
-
transition-all
|
|
57
|
-
duration-150
|
|
58
|
-
<%= (current_mode == mode) ? 'border-cyan-500 bg-cyan-50 dark:bg-cyan-900/40 dark:border-cyan-400' : 'border-slate-200 hover:border-slate-300 hover:bg-slate-50 dark:border-slate-700 dark:hover:border-slate-500 dark:hover:bg-slate-700' %>
|
|
59
|
-
"
|
|
21
|
+
aria-pressed="<%= current_mode == mode %>"
|
|
22
|
+
data-mode="<%= mode %>"
|
|
23
|
+
data-action="click->docs-mode#rememberFocus"
|
|
24
|
+
class="px-3 py-1.5 text-sm font-medium rounded-md border transition-colors <%= (current_mode == mode) ? 'border-cyan-500 bg-cyan-50 text-cyan-900 dark:bg-cyan-900/40 dark:border-cyan-400 dark:text-cyan-100' : 'border-slate-200 text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-700' %>"
|
|
60
25
|
>
|
|
61
|
-
|
|
62
|
-
block
|
|
63
|
-
font-medium
|
|
64
|
-
<%= (current_mode == mode) ? 'text-cyan-900 dark:text-cyan-100' : 'text-slate-900 dark:text-slate-100' %>
|
|
65
|
-
">
|
|
66
|
-
<%= t("markdowndocs.modes.#{mode}", default: mode.titleize) %>
|
|
67
|
-
</span>
|
|
68
|
-
<% description = t("markdowndocs.modes.#{mode}_description", default: "") %>
|
|
69
|
-
<% if description.present? %>
|
|
70
|
-
<p class="
|
|
71
|
-
text-xs
|
|
72
|
-
mt-1
|
|
73
|
-
<%= (current_mode == mode) ? 'text-cyan-900 dark:text-cyan-100' : 'text-slate-700 dark:text-slate-300' %>
|
|
74
|
-
">
|
|
75
|
-
<%= description %>
|
|
76
|
-
</p>
|
|
77
|
-
<% end %>
|
|
26
|
+
<%= t("markdowndocs.modes.#{mode}", default: mode.titleize) %>
|
|
78
27
|
</button>
|
|
79
28
|
<% end %>
|
|
80
29
|
<% end %>
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
<%# locals: (rendered_content:, related_docs:,
|
|
1
|
+
<%# locals: (rendered_content:, related_docs:, toc_items:) %>
|
|
2
2
|
<aside class="sidebar space-y-6">
|
|
3
|
-
<% if available_modes.length > 1 %>
|
|
4
|
-
<%= render "markdowndocs/docs/mode_switcher",
|
|
5
|
-
current_mode: current_mode,
|
|
6
|
-
available_modes: available_modes %>
|
|
7
|
-
<% end %>
|
|
8
|
-
|
|
9
3
|
<% if toc_items.length >= 3 %>
|
|
10
4
|
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-sm p-6">
|
|
11
5
|
<nav aria-label="Table of Contents">
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
data-controller="docs-search"
|
|
7
7
|
data-docs-search-index-url-value="<%= markdowndocs.search_index_path %>"
|
|
8
8
|
<% end %>>
|
|
9
|
+
<%= render "markdowndocs/docs/docs_toolbar",
|
|
10
|
+
current_mode: @docs_mode,
|
|
11
|
+
available_modes: @available_modes %>
|
|
9
12
|
<!-- Hero Section -->
|
|
10
13
|
<div class="text-center mb-12">
|
|
11
14
|
<h1 class="text-4xl font-bold text-gray-900 dark:text-slate-100 mb-4">Documentation</h1>
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
<!-- Breadcrumb -->
|
|
19
19
|
<%= render "markdowndocs/docs/breadcrumb", category: @doc.category, title: @doc.title %>
|
|
20
20
|
|
|
21
|
+
<%= render "markdowndocs/docs/docs_toolbar",
|
|
22
|
+
current_mode: @docs_mode,
|
|
23
|
+
available_modes: @available_modes %>
|
|
24
|
+
|
|
21
25
|
<!-- Mobile navigation (hamburger dropdown, hidden on desktop) -->
|
|
22
26
|
<div class="lg:hidden mb-6">
|
|
23
27
|
<button
|
|
@@ -57,8 +61,6 @@
|
|
|
57
61
|
<%= render "markdowndocs/docs/navigation",
|
|
58
62
|
rendered_content: @rendered_content,
|
|
59
63
|
related_docs: @related_docs,
|
|
60
|
-
current_mode: @docs_mode,
|
|
61
|
-
available_modes: @available_modes,
|
|
62
64
|
toc_items: @toc_items %>
|
|
63
65
|
</div>
|
|
64
66
|
</div>
|
|
@@ -84,8 +86,6 @@
|
|
|
84
86
|
<%= render "markdowndocs/docs/navigation",
|
|
85
87
|
rendered_content: @rendered_content,
|
|
86
88
|
related_docs: @related_docs,
|
|
87
|
-
current_mode: @docs_mode,
|
|
88
|
-
available_modes: @available_modes,
|
|
89
89
|
toc_items: @toc_items %>
|
|
90
90
|
</div>
|
|
91
91
|
</div>
|
data/config/locales/en.yml
CHANGED
|
@@ -4,6 +4,8 @@ en:
|
|
|
4
4
|
navigation_sidebar: "Navigation & Related Docs"
|
|
5
5
|
viewing_mode: "Viewing Mode"
|
|
6
6
|
select_viewing_mode: "Select documentation viewing mode"
|
|
7
|
+
mode_announcement: "Now viewing %{mode}."
|
|
8
|
+
no_counterpart_announcement: "No %{mode} version of this page — showing the %{mode} documentation."
|
|
7
9
|
modes:
|
|
8
10
|
guide: "User Guide"
|
|
9
11
|
guide_description: "User-friendly explanations and step-by-step instructions"
|
data/lib/markdowndocs/version.rb
CHANGED
data/mise.toml
ADDED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: markdowndocs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dave Chmura
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -75,6 +75,7 @@ executables: []
|
|
|
75
75
|
extensions: []
|
|
76
76
|
extra_rdoc_files: []
|
|
77
77
|
files:
|
|
78
|
+
- ".tool-versions"
|
|
78
79
|
- CHANGELOG.md
|
|
79
80
|
- README.md
|
|
80
81
|
- Rakefile
|
|
@@ -91,6 +92,7 @@ files:
|
|
|
91
92
|
- app/views/layouts/markdowndocs/application.html.erb
|
|
92
93
|
- app/views/markdowndocs/docs/_breadcrumb.html.erb
|
|
93
94
|
- app/views/markdowndocs/docs/_card.html.erb
|
|
95
|
+
- app/views/markdowndocs/docs/_docs_toolbar.html.erb
|
|
94
96
|
- app/views/markdowndocs/docs/_mode_switcher.html.erb
|
|
95
97
|
- app/views/markdowndocs/docs/_navigation.html.erb
|
|
96
98
|
- app/views/markdowndocs/docs/index.html.erb
|
|
@@ -104,6 +106,7 @@ files:
|
|
|
104
106
|
- lib/markdowndocs/configuration.rb
|
|
105
107
|
- lib/markdowndocs/engine.rb
|
|
106
108
|
- lib/markdowndocs/version.rb
|
|
109
|
+
- mise.toml
|
|
107
110
|
- sig/markdowndocs.rbs
|
|
108
111
|
homepage: https://github.com/dschmura/markdowndocs
|
|
109
112
|
licenses:
|