jekyll-theme-zer0 1.31.0 → 1.31.2

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.
@@ -11,6 +11,11 @@
11
11
  validity). The trail uses BreadcrumbList microdata, so it's eligible for
12
12
  Google rich results, and the first crumb label is sourced from
13
13
  site.data.ui-text for i18n.
14
+
15
+ Crumbs that aren't links are visible but omitted from the
16
+ BreadcrumbList, because a ListItem without a resolvable `item`
17
+ invalidates the list (#512). `bc_pos` counts only structured items,
18
+ so `position` stays contiguous (1..n).
14
19
  ===================================================================
15
20
  {%- endcomment -%}
16
21
  {% if page.url != "/" and site.breadcrumbs %}
@@ -19,6 +24,7 @@
19
24
  {%- assign label_aria = ui.breadcrumbs_aria | default: "Breadcrumb" -%}
20
25
  {%- assign crumbs = page.url | remove:'/index.html' | split: '/' -%}
21
26
  {%- assign section = crumbs[1] | default: '' -%}
27
+ {%- assign bc_pos = 1 -%}
22
28
 
23
29
  <nav class="breadcrumbs" aria-label="{{ label_aria }}">
24
30
  <ol class="breadcrumb mb-0" itemscope itemtype="https://schema.org/BreadcrumbList">
@@ -50,41 +56,41 @@
50
56
  {%- if _section_page -%}{%- break -%}{%- endif -%}
51
57
  {%- endfor -%}
52
58
  {%- endunless -%}
53
- <li class="breadcrumb-item" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
54
- {%- if _section_page -%}
59
+ {%- if _section_page -%}
60
+ {%- assign bc_pos = bc_pos | plus: 1 -%}
61
+ <li class="breadcrumb-item" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
55
62
  <a href="{{ _section_url | relative_url }}" itemprop="item">
56
63
  <span itemprop="name">{{ section_label }}</span>
57
64
  </a>
58
- {%- else -%}
59
- <span itemprop="name">{{ section_label }}</span>
60
- {%- endif -%}
61
- <meta itemprop="position" content="2" />
62
- </li>
65
+ <meta itemprop="position" content="{{ bc_pos }}" />
66
+ </li>
67
+ {%- else -%}
68
+ <li class="breadcrumb-item">{{ section_label }}</li>
69
+ {%- endif -%}
70
+ {%- assign bc_pos = bc_pos | plus: 1 -%}
63
71
  <li class="breadcrumb-item active" aria-current="page" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
64
72
  <span itemprop="name">{{ page.title }}</span>
65
- <meta itemprop="position" content="3" />
73
+ <meta itemprop="position" content="{{ bc_pos }}" />
66
74
  </li>
67
75
  {%- else -%}
68
76
  {%- for crumb in crumbs offset: 1 -%}
69
- {%- assign position = forloop.index | plus: 1 -%}
70
77
  {%- assign crumb_label = crumb | replace:'-',' ' | remove:'.html' | capitalize -%}
71
78
  {%- if forloop.last -%}
79
+ {%- assign bc_pos = bc_pos | plus: 1 -%}
72
80
  <li class="breadcrumb-item active" aria-current="page" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
73
81
  <span itemprop="name">{{ page.title }}</span>
74
- <meta itemprop="position" content="{{ position }}" />
82
+ <meta itemprop="position" content="{{ bc_pos }}" />
75
83
  </li>
76
84
  {%- elsif forloop.first -%}
85
+ {%- assign bc_pos = bc_pos | plus: 1 -%}
77
86
  <li class="breadcrumb-item" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
78
87
  <a href="{{ '/' | append: crumb | append: '/' | relative_url }}" itemprop="item">
79
88
  <span itemprop="name">{{ crumb_label }}</span>
80
89
  </a>
81
- <meta itemprop="position" content="{{ position }}" />
90
+ <meta itemprop="position" content="{{ bc_pos }}" />
82
91
  </li>
83
92
  {%- else -%}
84
- <li class="breadcrumb-item" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
85
- <span itemprop="name">{{ crumb_label }}</span>
86
- <meta itemprop="position" content="{{ position }}" />
87
- </li>
93
+ <li class="breadcrumb-item">{{ crumb_label }}</li>
88
94
  {%- endif -%}
89
95
  {%- endfor -%}
90
96
  {%- endif -%}
@@ -502,9 +502,12 @@
502
502
  position: relative;
503
503
  }
504
504
 
505
+ // 1.5rem (24px) is the WCAG 2.2 SC 2.5.8 target-size minimum: the
506
+ // chevron overlay below is this wide, so the link reserves the same.
507
+ // (It was 1.25rem = 20px, which axe flagged as `target-size`.)
505
508
  #bdNavbar .nav-hover-dropdown > .nav-link:first-child {
506
509
  border-radius: 0.375rem;
507
- padding-right: 1.25rem;
510
+ padding-right: 1.5rem;
508
511
  }
509
512
 
510
513
  #bdNavbar .nav-hover-dropdown > .dropdown-toggle-split {
@@ -516,9 +519,9 @@
516
519
  display: flex;
517
520
  align-items: center;
518
521
  justify-content: center;
519
- width: 1.25rem;
520
- min-width: 1.25rem;
521
- min-height: 0;
522
+ width: 1.5rem;
523
+ min-width: 1.5rem;
524
+ min-height: 1.5rem;
522
525
  margin: 0 !important;
523
526
  padding: 0;
524
527
  background-color: transparent;
@@ -1367,6 +1370,29 @@
1367
1370
  vertical-align: middle;
1368
1371
  }
1369
1372
 
1373
+ // Phones: at the 20px .navbar-brand size a typical title loses its tail
1374
+ // once the sidebar toggle, logo, search and labelled Menu toggle share the
1375
+ // bar ("Lifehacker.dev" -> "Lifehacker…" on post pages at 400px, 121px
1376
+ // available for 133px of text). Step the title down so it fits; the
1377
+ // ellipsis above still covers titles that are longer still. The logo's
1378
+ // Bootstrap margin-end (1rem) also stacks on the brand group's 0.5rem gap
1379
+ // there; the gap alone is enough spacing and hands the title 16px back.
1380
+ @media (max-width: 575.98px) {
1381
+ #navbar .site-title-text {
1382
+ font-size: 1rem;
1383
+ }
1384
+
1385
+ #navbar .navbar-brand-group > .navbar-brand {
1386
+ margin-right: 0;
1387
+ }
1388
+ }
1389
+
1390
+ @media (max-width: 374.98px) {
1391
+ #navbar .site-title-text {
1392
+ font-size: 0.9375rem;
1393
+ }
1394
+ }
1395
+
1370
1396
  #navbar .navbar-toggler {
1371
1397
  width: 2.5rem;
1372
1398
  height: 2.5rem;
@@ -78,11 +78,15 @@
78
78
  }
79
79
 
80
80
  // Links (Bootstrap + theme)
81
- a:not(.btn):not(.nav-link):not(.dropdown-item) {
81
+ // `.badge` is excluded so tag/category chips rendered as <a class="badge
82
+ // bg-*"> keep Bootstrap's badge text colour (--bs-badge-color, #fff) instead
83
+ // of the skin link colour. On neon that was #5e22cc on bg-primary #8338ec
84
+ // (1.45:1) and on bg-secondary (1.74:1).
85
+ a:not(.btn):not(.nav-link):not(.dropdown-item):not(.badge) {
82
86
  color: var(--bs-link-color);
83
87
  }
84
88
 
85
- a:not(.btn):hover {
89
+ a:not(.btn):not(.badge):hover {
86
90
  color: var(--bs-link-hover-color);
87
91
  }
88
92
 
@@ -39,6 +39,16 @@
39
39
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
40
40
  }
41
41
 
42
+ // The MODE the visitor has selected (light/dark/auto) — what the toggle
43
+ // buttons represent. Unlike getPreferredTheme() it never resolves `auto`
44
+ // to the OS scheme, so a first visit on an auto site marks Auto (#521).
45
+ const getPreferredMode = () => {
46
+ if (isLocked) {
47
+ return configDefault
48
+ }
49
+ return getStoredTheme() || configDefault
50
+ }
51
+
42
52
  const setTheme = theme => {
43
53
  if (theme === 'auto') {
44
54
  document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
@@ -95,7 +105,7 @@
95
105
  })
96
106
 
97
107
  window.addEventListener('DOMContentLoaded', () => {
98
- showActiveTheme(getPreferredTheme())
108
+ showActiveTheme(getPreferredMode())
99
109
 
100
110
  document.querySelectorAll('[data-bs-theme-value]')
101
111
  .forEach(toggle => {
@@ -73,8 +73,32 @@
73
73
  bootstrap.Modal.getOrCreateInstance(modalEl).show();
74
74
  };
75
75
 
76
- const openModal = () => {
76
+ // Where focus goes when the modal closes. Bootstrap only restores
77
+ // focus for data-bs-toggle triggers, and this modal is opened from
78
+ // code ("/", Ctrl/Cmd+K, the toggle's click handler), so on Escape
79
+ // focus used to fall back to <body> (WCAG 2.4.3 Focus Order).
80
+ let returnFocusTo = null;
81
+
82
+ const isVisible = (el) => !!el && el.isConnected && el.getClientRects().length > 0;
83
+
84
+ const rememberOpener = (preferred) => {
85
+ const active = document.activeElement;
86
+ if (preferred && isVisible(preferred)) {
87
+ returnFocusTo = preferred;
88
+ } else if (active && active !== document.body && !modalEl.contains(active) && isVisible(active)) {
89
+ returnFocusTo = active;
90
+ } else {
91
+ // Opened from the keyboard with nothing focused: return to
92
+ // the visible Search button, the control that owns the dialog.
93
+ returnFocusTo = Array.from(document.querySelectorAll('[data-search-toggle]')).find(isVisible) || null;
94
+ }
95
+ };
96
+
97
+ const openModal = (preferredOpener) => {
77
98
  if (typeof bootstrap === 'undefined') return;
99
+ if (!modalEl.classList.contains('show')) {
100
+ rememberOpener(preferredOpener instanceof Element ? preferredOpener : null);
101
+ }
78
102
  const cookieEl = document.getElementById('cookieSettingsModal');
79
103
  const infoEl = document.getElementById('info-section');
80
104
  const drawerEl = document.getElementById('zer0UnifiedDrawer');
@@ -102,13 +126,13 @@
102
126
  }
103
127
 
104
128
  // Open modal when keyboard shortcut requests search
105
- document.addEventListener('navigation:searchRequest', openModal);
129
+ document.addEventListener('navigation:searchRequest', () => openModal());
106
130
 
107
131
  // Open modal when clicking a search toggle button
108
132
  document.querySelectorAll('[data-search-toggle]').forEach((button) => {
109
133
  button.addEventListener('click', (event) => {
110
134
  event.preventDefault();
111
- openModal();
135
+ openModal(button);
112
136
  });
113
137
  });
114
138
 
@@ -152,6 +176,14 @@
152
176
  searchInput.value = '';
153
177
  }
154
178
  clearResults();
179
+ // Only restore focus if nothing else claimed it (e.g. a result
180
+ // link navigating away, or the info-section offcanvas opening).
181
+ const target = returnFocusTo;
182
+ returnFocusTo = null;
183
+ const active = document.activeElement;
184
+ if (target && isVisible(target) && (!active || active === document.body || modalEl.contains(active))) {
185
+ try { target.focus({ preventScroll: true }); } catch (e) { target.focus(); }
186
+ }
155
187
  });
156
188
 
157
189
  // Prevent empty submissions, and keep submissions in-modal when the
data/scripts/bin/validate CHANGED
@@ -415,6 +415,7 @@ end
415
415
  classified_files = %w[
416
416
  _config.yml
417
417
  _config_dev.yml
418
+ _config_snapshots.yml
418
419
  _config_secrets_local.yml
419
420
  pages/_about/settings/_config.yml
420
421
  .github/ISSUE_TEMPLATE/config.yml
@@ -9,7 +9,7 @@
9
9
  # .github/instructions/features.instructions.md.
10
10
  #
11
11
  # HARD failures (exit 1):
12
- # - features/features.yml and _data/features.yml are not byte-identical
12
+ # - _data/features.yml (the single registry) is missing
13
13
  # - the header is missing a `# Last Updated:` line
14
14
  # - schema violations (missing required field, bad id/version, duplicate id)
15
15
  # - implemented:false without `removed_in:`
@@ -28,7 +28,6 @@
28
28
  require 'date'
29
29
  require 'yaml'
30
30
 
31
- MASTER = 'features/features.yml'
32
31
  MIRROR = '_data/features.yml'
33
32
  REQUIRED = %w[id title description implemented version link docs tags date].freeze
34
33
  STRICT = ENV['FEATURES_STRICT'] == '1'
@@ -52,12 +51,10 @@ def ref_paths(refs)
52
51
  refs.values.flat_map { |v| v.is_a?(Array) ? v : [v] }.select { |v| v.is_a?(String) }
53
52
  end
54
53
 
55
- [MASTER, MIRROR].each { |f| die "#{f} is missing" unless File.file?(f) }
54
+ die "#{MIRROR} is missing" unless File.file?(MIRROR)
56
55
 
57
- # 1. Sync contract — the two registries must be byte-identical.
58
- if File.binread(MASTER) != File.binread(MIRROR)
59
- die "#{MASTER} and #{MIRROR} differ — run `cp #{MASTER} #{MIRROR}` (must be byte-identical)"
60
- end
56
+ # 1. Single registry — _data/features.yml is the only copy (the former
57
+ # features/features.yml master was removed as a duplicate, UPS-WORK-06).
61
58
 
62
59
  data = load_yaml(MIRROR)
63
60
  feats = data && data['features']
@@ -146,4 +143,4 @@ unless warnings.empty?
146
143
  end
147
144
 
148
145
  active = feats.count { |f| f['implemented'] != false }
149
- puts "Feature registry valid: #{feats.length} entries (#{active} active), refs resolved, master/_data in sync"
146
+ puts "Feature registry valid: #{feats.length} entries (#{active} active), refs resolved, _data registry (single copy)"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-theme-zer0
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.31.0
4
+ version: 1.31.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amr Abdel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-07 00:00:00.000000000 Z
11
+ date: 2026-10-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll