jekyll-theme-docsteer 1.0.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b49a45ae03e89044c653d5cae4fa3749d6b3c4a3d7c6837555a383242700ee3
4
- data.tar.gz: 1f9b744dc050ff685596ba7d853ed230cdf08224d4aa9caceead2363470ec14e
3
+ metadata.gz: efd8a4dee01456dd316c9a4ba45f0762ebed0255b192413774687b8f6ce7143e
4
+ data.tar.gz: 7fc1744dcf4232e345625ad934ed9d9fd4468f705ca5099fe79d4233e980331a
5
5
  SHA512:
6
- metadata.gz: c506de353cb020505952ca9bd4b6c7e69f1bddc933c538e0d1bbc58beaf0f01cedddf9becacf21ca7c2dc17677a6c2cb028a0a56533fa41ecba9af3463f2cfa7
7
- data.tar.gz: 046a10b8e0e8814a576703ef264c368ae356464d71cb28baf83ed2bf4f2ba4cf44000912a9363a14ee162fd8c1c63445d38f51174a5cd1aff6d5e27fdafb9854
6
+ metadata.gz: 66657360820626321676e5100f51c159ba900fb00de2b22c253f68036889120bc9a00c8c19f9079f74ca7db6c9cdf7d84fa2e33c5669273e36f511cf250a839e
7
+ data.tar.gz: 39543eee91a6f6c3ed311941e5e5ab953bc7f9e0dabd8ef9feec253bd8933dc386f4924912d05aaf5ade2615b7a0d8ecef81a0b1db4828a07c96c52b1650e9e9
data/README.md CHANGED
@@ -100,6 +100,12 @@ GitHub Pages (via Actions), Netlify, Vercel, Cloudflare Pages — see
100
100
  (`footer.show_credit: false` to remove it).
101
101
  Font Awesome is under its own [Free License](https://fontawesome.com/license/free).
102
102
 
103
+ ## Roadmap
104
+
105
+ Planned work, deferred decisions and the release checklist live in
106
+ [`ROADMAP.md`](ROADMAP.md). Released changes are in
107
+ [`CHANGELOG.md`](CHANGELOG.md).
108
+
103
109
  ## Support
104
110
 
105
111
  If DocSteer saved you time, you can
@@ -33,6 +33,18 @@
33
33
 
34
34
  {% if page.tags and page.tags.size > 0 %}
35
35
  <div class="doc-tags">
36
- {% for t in page.tags %}<span class="badge badge--soft">#{{ t }}</span>{% endfor %}
36
+ {%- comment -%}
37
+ Tags are already in search.json, so a tag is a search the reader has not run
38
+ yet: the button hands the term to the modal. With search off there is
39
+ nothing to hand it to, so it stays the inert badge it was.
40
+ {%- endcomment -%}
41
+ {% for t in page.tags %}
42
+ {%- if site.docsteer.search.enabled -%}
43
+ <button type="button" class="badge badge--soft doc-tag"
44
+ data-search-open="{{ t }}" aria-label="Search for {{ t }}">#{{ t }}</button>
45
+ {%- else -%}
46
+ <span class="badge badge--soft">#{{ t }}</span>
47
+ {%- endif -%}
48
+ {% endfor %}
37
49
  </div>
38
50
  {% endif %}
data/_includes/head.html CHANGED
@@ -39,7 +39,7 @@
39
39
  {% endif %}
40
40
  {% endif %}
41
41
 
42
- <link rel="stylesheet" href="{{ '/assets/css/main.css' | relative_url }}">
42
+ <link rel="stylesheet" href="{{ '/assets/css/main.css' | relative_url }}?v={{ ds_version }}">
43
43
 
44
44
  {% if site.feed.path %}
45
45
  <link rel="alternate" type="application/atom+xml" title="{{ site.title }}" href="{{ '/feed.xml' | relative_url }}">
@@ -1,7 +1,7 @@
1
1
  {%- assign cfg = site.docsteer -%}
2
2
  <header class="navbar" id="navbar">
3
3
  <div class="navbar__inner">
4
- <button class="navbar__burger" id="sidebarToggle" aria-label="Open menu" aria-expanded="false">
4
+ <button class="navbar__burger" id="sidebarToggle" aria-controls="sidebar" aria-label="Open menu" aria-expanded="false">
5
5
  <i class="fa-solid fa-bars" aria-hidden="true"></i>
6
6
  </button>
7
7
 
@@ -17,6 +17,6 @@
17
17
  }
18
18
  };
19
19
  </script>
20
- <script src="{{ '/assets/js/main.js' | relative_url }}" defer></script>
21
- {% if cfg.search.enabled %}<script src="{{ '/assets/js/search.js' | relative_url }}" defer></script>{% endif %}
22
- {% if cfg.lightbox.enabled %}<script src="{{ '/assets/js/lightbox.js' | relative_url }}" defer></script>{% endif %}
20
+ <script src="{{ '/assets/js/main.js' | relative_url }}?v={{ ds_version }}" defer></script>
21
+ {% if cfg.search.enabled %}<script src="{{ '/assets/js/search.js' | relative_url }}?v={{ ds_version }}" defer></script>{% endif %}
22
+ {% if cfg.lightbox.enabled %}<script src="{{ '/assets/js/lightbox.js' | relative_url }}?v={{ ds_version }}" defer></script>{% endif %}
@@ -1,5 +1,13 @@
1
1
  {%- assign cfg = site.docsteer -%}
2
- <aside class="sidebar{% if cfg.sidebar.sticky %} is-sticky{% endif %}" id="sidebar" aria-label="Documentation navigation">
2
+ {%- comment -%}
3
+ Rendered on EVERY page, because the navbar burger is shown on every page at
4
+ mobile widths and has to open something. `include.docs` says whether this
5
+ page also gets the documentation tree; when it is false the aside exists
6
+ only as the mobile drawer (see .sidebar--drawer-only).
7
+ {%- endcomment -%}
8
+ {%- assign show_docs = include.docs -%}
9
+ <aside class="sidebar{% unless show_docs %} sidebar--drawer-only{% endunless %}{% if cfg.sidebar.sticky and show_docs %} is-sticky{% endif %}"
10
+ id="sidebar" aria-label="Site navigation">
3
11
  <div class="sidebar__inner">
4
12
 
5
13
  {% if cfg.search.enabled %}
@@ -9,6 +17,24 @@
9
17
  </button>
10
18
  {% endif %}
11
19
 
20
+ {%- comment -%}
21
+ The navbar's primary links are hidden below 960px, so the drawer carries
22
+ them. Hidden again on desktop, where the navbar shows them itself.
23
+ {%- endcomment -%}
24
+ <nav class="sidebar__primary" aria-label="Primary">
25
+ <a href="{{ '/' | relative_url }}"
26
+ class="sidebar__link{% if page.url == '/' %} is-active{% endif %}">
27
+ <i class="fa-solid fa-house" aria-hidden="true"></i>Home
28
+ </a>
29
+ {% for item in site.data.navigation.main %}
30
+ <a href="{{ item.url | relative_url }}"
31
+ class="sidebar__link{% if page.url contains item.url and item.url != '/' %} is-active{% endif %}">
32
+ {% if item.icon %}<i class="{{ item.icon }}" aria-hidden="true"></i>{% endif %}{{ item.title }}
33
+ </a>
34
+ {% endfor %}
35
+ </nav>
36
+
37
+ {% if show_docs %}
12
38
  <nav class="sidebar__nav">
13
39
  {% for group in site.data.navigation.sidebar %}
14
40
  {%- assign group_active = false -%}
@@ -40,6 +66,7 @@
40
66
  </div>
41
67
  {% endfor %}
42
68
  </nav>
69
+ {% endif %}
43
70
 
44
71
  {% if cfg.buy_me_a_coffee.username and cfg.buy_me_a_coffee.show_in contains "sidebar" %}
45
72
  <div class="sidebar__coffee">{% include buy-me-a-coffee.html %}</div>
@@ -2,6 +2,13 @@
2
2
  {%- assign cfg = site.docsteer -%}
3
3
  {%- assign skin = cfg.skin | default: "aqua" -%}
4
4
  {%- assign mode = cfg.mode | default: "auto" -%}
5
+ {%- comment -%}
6
+ Cache-busting token for the theme's own CSS and JS. Bumped with spec.version
7
+ in jekyll-theme-docsteer.gemspec: keyed to the theme's version rather than to
8
+ site.time so a content-only rebuild does not force every visitor to
9
+ re-download assets that did not change.
10
+ {%- endcomment -%}
11
+ {%- assign ds_version = "1.1.0" -%}
5
12
  <html lang="{{ site.lang | default: 'en' }}"
6
13
  data-skin="{{ skin }}"
7
14
  {% if mode != "auto" %}data-mode="{{ mode }}"{% endif %}>
@@ -24,8 +31,14 @@
24
31
  {%- assign show_sidebar = true -%}
25
32
  {%- endif -%}
26
33
 
34
+ {%- comment -%}
35
+ The aside is rendered on every page: below 960px it is the drawer the
36
+ navbar burger opens, and a page with no drawer leaves the burger dead and
37
+ the primary nav unreachable. Only `has-sidebar` — the desktop grid column —
38
+ is conditional.
39
+ {%- endcomment -%}
27
40
  <div class="page-shell{% if show_sidebar %} has-sidebar{% endif %}">
28
- {% if show_sidebar %}{% include sidebar.html %}{% endif %}
41
+ {% include sidebar.html docs=show_sidebar %}
29
42
 
30
43
  <main id="main" class="page-main{% if show_sidebar %} has-sidebar{% endif %}" tabindex="-1">
31
44
  {{ content }}
data/_layouts/doc.html CHANGED
@@ -10,9 +10,18 @@ layout: default
10
10
  {% if page.description %}<p class="doc-head__lead">{{ page.description }}</p>{% endif %}
11
11
 
12
12
  <div class="doc-head__meta">
13
- {% if page.updated or page.date %}
13
+ {%- comment -%}
14
+ `last_modified_at` is the field jekyll-seo-tag reads for dateModified,
15
+ so using it here keeps this row and the structured data on one source.
16
+ `updated` is kept as a fallback for pages written against DocSteer 1.0.
17
+ `page.date` is last on purpose: Jekyll invents one equal to the build
18
+ time for collection documents that have none, so it is a date only in
19
+ the sense that it parses.
20
+ {%- endcomment -%}
21
+ {%- assign doc_date = page.last_modified_at | default: page.updated | default: page.date -%}
22
+ {% if doc_date %}
14
23
  <span><i class="fa-regular fa-clock" aria-hidden="true"></i>
15
- Updated {{ page.updated | default: page.date | date: "%b %-d, %Y" }}</span>
24
+ Updated {{ doc_date | date: "%b %-d, %Y" }}</span>
16
25
  {% endif %}
17
26
  {% if page.read_time %}
18
27
  <span><i class="fa-regular fa-file-lines" aria-hidden="true"></i> {{ page.read_time }} min read</span>
@@ -293,6 +293,15 @@ h4:hover > .heading-anchor, .heading-anchor:focus-visible { opacity: 1; }
293
293
  .doc-pager__title { font-weight: 600; color: var(--brand); line-height: 1.4; }
294
294
 
295
295
  .doc-tags { margin-top: var(--sp-6); display: flex; flex-wrap: wrap; gap: var(--sp-2); }
296
+ .doc-tag {
297
+ transition: color var(--dur) var(--ease), background var(--dur) var(--ease),
298
+ border-color var(--dur) var(--ease);
299
+ }
300
+ .doc-tag:hover {
301
+ color: var(--brand-contrast);
302
+ background: var(--brand);
303
+ border-color: transparent;
304
+ }
296
305
 
297
306
  @media (max-width: 560px) {
298
307
  .doc-pager { grid-template-columns: 1fr; }
@@ -182,3 +182,29 @@
182
182
  .hero { padding-block: var(--sp-7) var(--sp-6); }
183
183
  .hero__search { margin-top: var(--sp-6); }
184
184
  }
185
+
186
+ @media (max-width: 560px) {
187
+ // Narrow enough that .link-section__grid has collapsed to one column. A
188
+ // full-width tile with the icon stacked above two short lines is mostly
189
+ // empty space, so the icon moves into a left column and the tile becomes as
190
+ // short as its text. Tiles configured without an icon keep the stacked
191
+ // layout — there is no first column for them to fill.
192
+ // Tiles this short need the gap between them to read as bigger than the gap
193
+ // between a tile's own two lines, or the column reads as one block.
194
+ .link-section__grid { gap: var(--sp-4); }
195
+
196
+ .link-tile:has(> i) {
197
+ grid-template-columns: auto minmax(0, 1fr);
198
+ align-items: center;
199
+ column-gap: var(--sp-4);
200
+ row-gap: var(--sp-1);
201
+ padding: var(--sp-4);
202
+ }
203
+ .link-tile:has(> i) > i {
204
+ grid-row: 1 / span 2;
205
+ margin-bottom: 0;
206
+ font-size: 1.05rem;
207
+ }
208
+ .link-tile:has(> i) > .link-tile__title,
209
+ .link-tile:has(> i) > .link-tile__text { grid-column: 2; }
210
+ }
@@ -28,6 +28,15 @@
28
28
  box-shadow: 0 24px 80px hsl(0 0% 0% / .5);
29
29
  animation: pop .18s var(--ease);
30
30
  }
31
+ // An SVG carrying only a viewBox has no intrinsic size, so the rules above —
32
+ // which cap, never grow — leave it at the browser's 300x150 default: it would
33
+ // open SMALLER than it appears in the page. Raster images are deliberately not
34
+ // covered, since stretching them would just upscale a small screenshot into a
35
+ // blurry mess. Authors can also avoid this by putting width/height on the SVG.
36
+ .lightbox__img[src$=".svg"] {
37
+ width: min(1100px, 94vw);
38
+ height: auto;
39
+ }
31
40
  .lightbox__caption { color: hsl(0 0% 100% / .82); font-size: .88rem; text-align: center; }
32
41
 
33
42
  .lightbox__btn {
@@ -164,6 +164,21 @@
164
164
  .navbar__links { display: none; }
165
165
  }
166
166
  @media (max-width: 560px) {
167
- .brand__name { display: none; }
168
167
  .navbar__inner { gap: var(--sp-1); padding-inline: var(--sp-4); }
168
+
169
+ // The navbar is the only place on a phone that names the site — the hero
170
+ // headline is the page's title, not the project's. So the name stays and the
171
+ // brand is the one thing allowed to give way: the icon buttons hold their
172
+ // size, and a title too long for the row truncates instead of disappearing.
173
+ .brand { min-width: 0; }
174
+ .brand__name {
175
+ min-width: 0;
176
+ overflow: hidden;
177
+ text-overflow: ellipsis;
178
+ white-space: nowrap;
179
+ }
180
+ .navbar__burger,
181
+ .brand__mark,
182
+ .brand__logo,
183
+ .navbar__tools { flex: none; }
169
184
  }
@@ -8,6 +8,7 @@
8
8
  // (and runs the border the whole height of the page, which looks better).
9
9
  align-self: stretch;
10
10
  }
11
+ .sidebar--drawer-only { display: none; }
11
12
  .sidebar.is-sticky .sidebar__inner {
12
13
  position: sticky;
13
14
  top: var(--nav-h);
@@ -53,6 +54,11 @@
53
54
  }
54
55
  .sidebar__search i { font-size: .85em; }
55
56
 
57
+ // ── Primary nav (drawer only) ─────────────────────
58
+ // The navbar hides its own links below 960px, so the drawer carries them.
59
+ // Above that the navbar shows them and this would only be a duplicate.
60
+ .sidebar__primary { display: none; }
61
+
56
62
  // ── Groups ───────────────────────────────────────────────────
57
63
  .sidebar__nav { display: grid; gap: var(--sp-1); }
58
64
 
@@ -156,4 +162,20 @@
156
162
  .sidebar.is-sticky .sidebar__inner { position: static; max-height: none; }
157
163
  .sidebar__inner { padding-top: calc(var(--nav-h) + var(--sp-4)); height: 100%; }
158
164
  .sidebar__link { min-height: 38px; }
165
+
166
+ // Pages with no documentation tree still render the aside, purely so the
167
+ // burger has a drawer to open. It exists only at drawer widths.
168
+ .sidebar--drawer-only { display: block; }
169
+
170
+ .sidebar__primary { display: grid; gap: 2px; }
171
+ .sidebar__primary .sidebar__link { gap: var(--sp-3); font-size: var(--fs-md); }
172
+ .sidebar__primary .sidebar__link > i { width: 1.1em; text-align: center; opacity: .8; }
173
+ // The active rail is drawn on the group's border-left, which this nav has no
174
+ // list to hang off — the marker would float in the padding.
175
+ .sidebar__primary .sidebar__link.is-active::before { content: none; }
176
+ .sidebar__primary + .sidebar__nav {
177
+ margin-top: var(--sp-5);
178
+ padding-top: var(--sp-5);
179
+ border-top: 1px solid var(--border);
180
+ }
159
181
  }
data/assets/js/search.js CHANGED
@@ -17,14 +17,16 @@
17
17
 
18
18
  var MAX = parseInt(list.dataset.max, 10) || DocSteer.search.max || 8;
19
19
  var index = null;
20
- var loading = false;
20
+ var indexPromise = null;
21
21
  var activeIdx = -1;
22
22
  var lastFocus = null;
23
23
 
24
+ // The in-flight promise is what gets cached, not a boolean: a second caller
25
+ // arriving mid-fetch has to wait for the same request, and the old `loading`
26
+ // flag resolved it with a null index instead.
24
27
  function loadIndex() {
25
- if (index || loading) return Promise.resolve(index);
26
- loading = true;
27
- return fetch(list.dataset.endpoint || DocSteer.search.endpoint)
28
+ if (indexPromise) return indexPromise;
29
+ indexPromise = fetch(list.dataset.endpoint || DocSteer.search.endpoint)
28
30
  .then(function (r) { return r.json(); })
29
31
  .then(function (data) {
30
32
  index = data.map(function (d) {
@@ -37,10 +39,10 @@
37
39
  (d.category || "") + " " + (d.content || "")).toLowerCase()
38
40
  };
39
41
  });
40
- loading = false;
41
42
  return index;
42
43
  })
43
- .catch(function () { loading = false; return []; });
44
+ .catch(function () { indexPromise = null; return []; });
45
+ return indexPromise;
44
46
  }
45
47
 
46
48
  function scoreItem(item, terms, raw) {
@@ -118,37 +120,47 @@
118
120
  });
119
121
  }
120
122
 
123
+ function runSearch(raw) {
124
+ if (raw.length < 2) {
125
+ list.innerHTML = "";
126
+ empty.hidden = true;
127
+ hint.hidden = false;
128
+ input.setAttribute("aria-expanded", "false");
129
+ return;
130
+ }
131
+ loadIndex().then(function (idx) {
132
+ var terms = raw.split(/\s+/);
133
+ var scored = [];
134
+ for (var i = 0; i < idx.length; i++) {
135
+ var s = scoreItem(idx[i], terms, raw);
136
+ if (s > 0) scored.push({ item: idx[i], s: s });
137
+ }
138
+ scored.sort(function (a, b) { return b.s - a.s; });
139
+ render(scored.slice(0, MAX).map(function (x) { return x.item; }), terms);
140
+ });
141
+ }
142
+
121
143
  var debounce;
122
144
  function onInput() {
123
145
  clearTimeout(debounce);
124
146
  debounce = setTimeout(function () {
125
- var raw = input.value.trim().toLowerCase();
126
- if (raw.length < 2) {
127
- list.innerHTML = "";
128
- empty.hidden = true;
129
- hint.hidden = false;
130
- input.setAttribute("aria-expanded", "false");
131
- return;
132
- }
133
- loadIndex().then(function (idx) {
134
- var terms = raw.split(/\s+/);
135
- var scored = [];
136
- for (var i = 0; i < idx.length; i++) {
137
- var s = scoreItem(idx[i], terms, raw);
138
- if (s > 0) scored.push({ item: idx[i], s: s });
139
- }
140
- scored.sort(function (a, b) { return b.s - a.s; });
141
- render(scored.slice(0, MAX).map(function (x) { return x.item; }), terms);
142
- });
147
+ runSearch(input.value.trim().toLowerCase());
143
148
  }, 90);
144
149
  }
145
150
 
146
- function open() {
151
+ // `query` opens the modal on a term rather than on an empty field — the doc
152
+ // footer's tags use it, so a tag lands on its results instead of on a blank
153
+ // search. select() leaves it highlighted, so typing still replaces it.
154
+ function open(query) {
147
155
  lastFocus = document.activeElement;
148
156
  modal.classList.add("is-open");
149
157
  modal.setAttribute("aria-hidden", "false");
150
158
  document.body.style.overflow = "hidden";
151
159
  loadIndex();
160
+ if (query) {
161
+ input.value = query;
162
+ runSearch(query.trim().toLowerCase());
163
+ }
152
164
  setTimeout(function () { input.focus(); input.select(); }, 20);
153
165
  }
154
166
  function close() {
@@ -159,7 +171,10 @@
159
171
  }
160
172
 
161
173
  $$("[data-search-open]").forEach(function (b) {
162
- b.addEventListener("click", function (e) { e.preventDefault(); open(); });
174
+ b.addEventListener("click", function (e) {
175
+ e.preventDefault();
176
+ open(b.getAttribute("data-search-open"));
177
+ });
163
178
  });
164
179
  $$("[data-search-close]").forEach(function (b) {
165
180
  b.addEventListener("click", close);
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-theme-docsteer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cristian Castellari
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-09-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: jekyll
@@ -168,7 +167,6 @@ metadata:
168
167
  plugin_type: theme
169
168
  source_code_uri: https://github.com/Skyflash/docsteer
170
169
  bug_tracker_uri: https://github.com/Skyflash/docsteer/issues
171
- post_install_message:
172
170
  rdoc_options: []
173
171
  require_paths:
174
172
  - lib
@@ -183,8 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
181
  - !ruby/object:Gem::Version
184
182
  version: '0'
185
183
  requirements: []
186
- rubygems_version: 3.3.26
187
- signing_key:
184
+ rubygems_version: 3.6.9
188
185
  specification_version: 4
189
186
  summary: Simple, searchable docs and knowledge bases for teams.
190
187
  test_files: []