al_folio_core 1.0.3 → 1.0.4

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: a15b5c9e5bcd706d7d8fe4905dc6808ff410a61e75ab5e9f56005d55701a09eb
4
- data.tar.gz: d85d3073a73190a3de3d848f344a963de62a6ddc9820e8d5ef3a7d59a5892721
3
+ metadata.gz: af6ef2628760004262fc0a4af6b03c55e4cd4c25c7be638d7bdd6c60954cf4b1
4
+ data.tar.gz: b26e87c086ddd099bcfd91bc14cfcbfa84b9340cd70ddb1548586951151d4d61
5
5
  SHA512:
6
- metadata.gz: 682d3c7b5226138717d8519612f8a619a72935dd901ec957375e0497edb1f8558e7ea54bb048d781d8c706e55b58241416488d2f22fa66943d9469db38a8f864
7
- data.tar.gz: 8112481419f442d856e68349c4e58279fc53c37dd9a03460280b86a7389ce67c8ff2be1a40ff9364866030fe7f4987fb5976f60ce732c0527909e447f1f07dcc
6
+ metadata.gz: da5cc4a13562f97f0c0a7930620c616a1cd95d8022d070beb6cac66cbe91aa16f964dca91df7ee55fbe2f75ace2c03a454d064b782f2d33c267e59da8c9b7c71
7
+ data.tar.gz: cdc0e5b066d1aa470af9abacd227129b32bed674c43ef4d7b3a8d4c790d58b68323273290528df572bc85c9a7a69d30760e70af285eda1a60c41059c2dad3e2e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.4 - 2026-02-17
4
+
5
+ - Fixed related-posts HTML structure to render valid list markup.
6
+ - Restored sidebar TOC behavior and styling via Tocbot runtime integration.
7
+ - Added Tailwind-first vanilla table engine for `pretty_table` pages when Bootstrap compatibility is disabled.
8
+ - Replaced remaining jQuery-dependent runtime scripts (masonry, jupyter link handling) with vanilla JS.
9
+ - Improved project hover lift, teaching calendar toggle UX, and schedule/table styling parity.
10
+
3
11
  ## 1.0.3 - 2026-02-17
4
12
 
5
13
  - Restricted gem packaging to tracked runtime files to prevent accidental inclusion of local/untracked artifacts.
@@ -3,9 +3,11 @@
3
3
  <h2 class="mb-3"><i class="fas fa-calendar-alt"></i> Upcoming Events</h2>
4
4
 
5
5
  <button
6
- class="btn btn-sm btn-outline-primary mb-3"
6
+ class="btn btn-sm btn-outline-primary mb-3 calendar-toggle-btn"
7
+ type="button"
7
8
  onclick="toggleCalendar()"
8
9
  id="calendar-toggle-btn"
10
+ aria-pressed="false"
9
11
  >
10
12
  Show Calendar
11
13
  </button>
@@ -33,6 +33,17 @@
33
33
  >
34
34
  {% endif %}
35
35
 
36
+ {% if page.toc and page.toc.sidebar %}
37
+ <!-- Tocbot -->
38
+ <link
39
+ defer
40
+ rel="stylesheet"
41
+ href="{{ site.third_party_libraries.tocbot.url.css }}"
42
+ integrity="{{ site.third_party_libraries.tocbot.integrity.css }}"
43
+ crossorigin="anonymous"
44
+ >
45
+ {% endif %}
46
+
36
47
  <!-- Icons -->
37
48
  {% if site.plugins contains 'al_icons' %}
38
49
  {% include plugins/al_icons_styles.liquid %}
@@ -119,7 +119,7 @@
119
119
  {% if site.search_enabled %}
120
120
  <!-- Search -->
121
121
  <li class="nav-item">
122
- <button id="search-toggle" title="Search" onclick="openSearchModal()">
122
+ <button id="search-toggle" type="button" title="Search" aria-label="Open search">
123
123
  <span class="nav-link">ctrl k <i class="fa-solid fa-magnifying-glass"></i></span>
124
124
  </button>
125
125
  </li>
@@ -127,7 +127,7 @@
127
127
  {% if site.enable_darkmode %}
128
128
  <!-- Toogle theme mode -->
129
129
  <li class="toggle-container">
130
- <button id="light-toggle" title="Change theme">
130
+ <button id="light-toggle" type="button" title="Change theme" aria-label="Change color theme">
131
131
  <i class="fa-half-sun-moon" id="light-toggle-system"></i>
132
132
  <i class="fa-solid fa-moon" id="light-toggle-dark"></i>
133
133
  <i class="fa-solid fa-sun" id="light-toggle-light"></i>
@@ -1,35 +1,42 @@
1
- {% assign have_related_posts = false %}
2
- {% for post in site.related_posts | limit: site.related_blog_posts.max_related %}
3
- {% unless have_related_posts %}
4
- {% assign have_related_posts = true %}
5
- {% if page.layout == 'post' %}
6
- <br>
7
- <hr>
8
- <br>
9
- <ul class="list-disc pl-8"></ul>
10
-
11
- <!-- Adds related posts to the end of an article -->
12
- <h2 class="text-3xl font-semibold mb-4 mt-12">Enjoy Reading This Article?</h2>
13
- {% else %}
14
- <h2 class="text-3xl font-semibold mb-4 mt-12">Enjoy Reading This Article?</h2>
15
- {% endif %}
1
+ {% assign related_posts = site.related_posts | limit: site.related_blog_posts.max_related %}
2
+ {% if related_posts != empty %}
3
+ {% if page.layout == 'post' %}
4
+ <br>
5
+ <hr>
6
+ <br>
7
+ {% endif %}
16
8
 
17
- <p class="mb-2">Here are some more articles you might like to read next:</p>
18
- {% endunless %}
9
+ <h2 class="text-3xl font-semibold mb-4 mt-12">Enjoy Reading This Article?</h2>
10
+ <p class="mb-2">Here are some more articles you might like to read next:</p>
19
11
 
20
- <li class="my-2">
21
- {% if post.redirect == blank %}
22
- <a class="text-pink-700 underline font-semibold hover:text-pink-800" href="{{ post.url | relative_url }}">{{ post.title }}</a>
23
- {% elsif post.redirect contains '://' %}
24
- <a class="text-pink-700 underline font-semibold hover:text-pink-800" href="{{ post.redirect }}" target="_blank">{{ post.title }}</a>
25
- <svg width="1rem" height="1rem" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg">
26
- <path d="M17 13.5v6H5v-12h6m3-3h6v6m0-6-9 9" class="icon_svg-stroke" stroke="#999" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path>
27
- </svg>
28
- {% else %}
29
- <a class="text-pink-700 underline font-semibold hover:text-pink-800" href="{{ post.redirect | relative_url }}">{{ post.title }}</a>
30
- {% endif %}
31
- </li>
32
- {% endfor %}
12
+ <ul class="list-disc pl-7 my-3 space-y-2 marker:text-[var(--global-theme-color)]">
13
+ {% for post in related_posts %}
14
+ <li class="leading-relaxed">
15
+ {% if post.redirect == blank %}
16
+ <a class="font-semibold underline decoration-1 hover:text-[var(--global-hover-color)]" href="{{ post.url | relative_url }}">
17
+ {{- post.title -}}
18
+ </a>
19
+ {% elsif post.redirect contains '://' %}
20
+ <a
21
+ class="font-semibold underline decoration-1 hover:text-[var(--global-hover-color)]"
22
+ href="{{ post.redirect }}"
23
+ target="_blank"
24
+ rel="noopener noreferrer"
25
+ >
26
+ {{- post.title -}}
27
+ </a>
28
+ <svg width="1rem" height="1rem" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
29
+ <path d="M17 13.5v6H5v-12h6m3-3h6v6m0-6-9 9" class="icon_svg-stroke" stroke="#999" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path>
30
+ </svg>
31
+ {% else %}
32
+ <a class="font-semibold underline decoration-1 hover:text-[var(--global-hover-color)]" href="{{ post.redirect | relative_url }}">
33
+ {{- post.title -}}
34
+ </a>
35
+ {% endif %}
36
+ </li>
37
+ {% endfor %}
38
+ </ul>
39
+ {% endif %}
33
40
  {% if site.newsletter.enabled and site.footer_fixed %}
34
41
  <p class="mb-2" style="margin-top: 1.5rem !important">Subscribe to be notified of future articles:</p>
35
42
  {% if site.plugins contains 'al_newsletter' %}
@@ -60,6 +60,19 @@
60
60
  crossorigin="anonymous"
61
61
  ></script>
62
62
  {% endif %}
63
+ {% if page.pretty_table and site.al_folio.compat.bootstrap.enabled != true %}
64
+ <script defer src="{{ '/assets/js/table-engine.js' | relative_url | bust_file_cache }}" type="text/javascript"></script>
65
+ {% endif %}
66
+
67
+ {% if page.toc and page.toc.sidebar %}
68
+ <!-- Tocbot -->
69
+ <script
70
+ defer
71
+ src="{{ site.third_party_libraries.tocbot.url.js }}"
72
+ integrity="{{ site.third_party_libraries.tocbot.integrity.js }}"
73
+ crossorigin="anonymous"
74
+ ></script>
75
+ {% endif %}
63
76
 
64
77
  <!-- Load Common JS -->
65
78
  <script src="{{ '/assets/js/no_defer.js' | relative_url | bust_file_cache }}"></script>
@@ -29,14 +29,14 @@
29
29
  <div class="col-sm-9">{{ content }}</div>
30
30
  <!-- sidebar, which will move to the top on a small screen -->
31
31
  <div class="col-sm-3">
32
- <nav id="toc-sidebar" class="sticky-top"></nav>
32
+ <nav id="toc-sidebar" class="sticky-top toc-sidebar" aria-label="Table of contents"></nav>
33
33
  </div>
34
34
  </div>
35
35
  {% else %}
36
36
  <div class="row">
37
37
  <!-- sidebar, which will move to the top on a small screen -->
38
38
  <div class="col-sm-3">
39
- <nav id="toc-sidebar" class="sticky-top"></nav>
39
+ <nav id="toc-sidebar" class="sticky-top toc-sidebar" aria-label="Table of contents"></nav>
40
40
  </div>
41
41
  <!-- main content area -->
42
42
  <div class="col-sm-9">{{ content }}</div>
@@ -53,6 +53,29 @@ ul.task-list input[type="checkbox"] {
53
53
  }
54
54
  }
55
55
 
56
+ .hoverable {
57
+ transition:
58
+ transform 0.22s ease,
59
+ box-shadow 0.22s ease;
60
+ }
61
+
62
+ .hoverable:hover {
63
+ transform: translateY(-4px);
64
+ box-shadow:
65
+ 0 10px 22px rgba(0, 0, 0, 0.12),
66
+ 0 4px 10px rgba(0, 0, 0, 0.1);
67
+ }
68
+
69
+ @media (prefers-reduced-motion: reduce) {
70
+ .hoverable {
71
+ transition: none;
72
+ }
73
+
74
+ .hoverable:hover {
75
+ transform: none;
76
+ }
77
+ }
78
+
56
79
  // Profile
57
80
 
58
81
  .profile {
data/_sass/_navbar.scss CHANGED
@@ -160,6 +160,7 @@
160
160
  border: 0;
161
161
  background-color: inherit;
162
162
  color: var(--global-text-color);
163
+ cursor: pointer;
163
164
  /* Fix footprint to prevent navbar shifting when icon changes */
164
165
  width: 2rem;
165
166
  height: 2rem;
@@ -183,6 +184,7 @@
183
184
  border: 0;
184
185
  background-color: inherit;
185
186
  color: var(--global-text-color);
187
+ cursor: pointer;
186
188
 
187
189
  &:hover {
188
190
  color: var(--global-hover-color);
@@ -63,6 +63,24 @@
63
63
 
64
64
  .course-schedule {
65
65
  margin-bottom: 30px;
66
+
67
+ table.table {
68
+ margin-top: 0.75rem;
69
+ }
70
+
71
+ table.table thead th {
72
+ background-color: color-mix(in srgb, var(--global-theme-color) 12%, transparent);
73
+ border-top: 0;
74
+ border-bottom: 1px solid var(--global-divider-color);
75
+ color: var(--global-text-color);
76
+ font-weight: 600;
77
+ letter-spacing: 0.01em;
78
+ padding: 0.7rem 1rem;
79
+ }
80
+
81
+ table.table tbody td {
82
+ padding: 0.72rem 1rem;
83
+ }
66
84
  }
67
85
 
68
86
  .schedule-description {
@@ -23,15 +23,16 @@ hr {
23
23
  border-top: 1px solid var(--global-divider-color);
24
24
  }
25
25
 
26
- table {
26
+ table:not(.table) {
27
27
  td,
28
28
  th {
29
29
  font-size: 1rem;
30
- padding: 1px 1rem 1px 0;
30
+ padding: 0.45rem 1rem 0.45rem 0;
31
+ border-top: 1px solid var(--global-divider-color);
31
32
  }
32
33
 
33
34
  th {
34
- font-weight: bold;
35
+ font-weight: 600;
35
36
  }
36
37
  }
37
38
 
@@ -177,29 +177,151 @@ html.transition *:after {
177
177
  }
178
178
  }
179
179
 
180
+ // Tailwind table engine
181
+
182
+ .af-table-shell {
183
+ margin: 1.1rem 0 1.4rem;
184
+ }
185
+
186
+ .af-table-toolbar {
187
+ display: flex;
188
+ flex-wrap: wrap;
189
+ align-items: center;
190
+ justify-content: space-between;
191
+ gap: 0.7rem;
192
+ margin-bottom: 0.6rem;
193
+ }
194
+
195
+ .af-table-search {
196
+ min-width: min(300px, 100%);
197
+ max-width: 100%;
198
+ border: 1px solid var(--global-divider-color);
199
+ border-radius: 0.4rem;
200
+ background-color: var(--global-bg-color);
201
+ color: var(--global-text-color);
202
+ font-size: 0.94rem;
203
+ line-height: 1.4;
204
+ padding: 0.42rem 0.72rem;
205
+ }
206
+
207
+ .af-table-search:focus-visible {
208
+ outline: 2px solid color-mix(in srgb, var(--global-theme-color) 35%, transparent);
209
+ outline-offset: 1px;
210
+ }
211
+
212
+ .af-table-pagination {
213
+ display: inline-flex;
214
+ align-items: center;
215
+ gap: 0.35rem;
216
+ }
217
+
218
+ .af-table-page-info {
219
+ color: var(--global-text-color);
220
+ font-size: 0.85rem;
221
+ font-weight: 500;
222
+ margin-right: 0.3rem;
223
+ }
224
+
225
+ .af-table-pagination button {
226
+ border: 1px solid var(--global-divider-color);
227
+ background-color: transparent;
228
+ border-radius: 0.35rem;
229
+ color: var(--global-text-color);
230
+ cursor: pointer;
231
+ font-size: 0.85rem;
232
+ font-weight: 500;
233
+ line-height: 1;
234
+ min-width: 2rem;
235
+ padding: 0.38rem 0.56rem;
236
+ transition:
237
+ background-color 0.2s ease,
238
+ color 0.2s ease,
239
+ border-color 0.2s ease;
240
+ }
241
+
242
+ .af-table-pagination button:hover:not(:disabled) {
243
+ background-color: var(--global-theme-color);
244
+ border-color: var(--global-theme-color);
245
+ color: var(--global-hover-text-color);
246
+ }
247
+
248
+ .af-table-pagination button:disabled {
249
+ cursor: default;
250
+ opacity: 0.45;
251
+ }
252
+
253
+ table[data-toggle="table"].af-table-enhanced {
254
+ margin-bottom: 0;
255
+ }
256
+
257
+ table[data-toggle="table"].af-table-enhanced thead th {
258
+ background-color: color-mix(in srgb, var(--global-theme-color) 12%, transparent);
259
+ border-bottom: 1px solid var(--global-divider-color);
260
+ color: var(--global-text-color);
261
+ font-weight: 600;
262
+ letter-spacing: 0.01em;
263
+ }
264
+
265
+ table[data-toggle="table"].af-table-enhanced th.af-sortable {
266
+ cursor: pointer;
267
+ user-select: none;
268
+ }
269
+
270
+ table[data-toggle="table"].af-table-enhanced th.af-sortable:hover {
271
+ color: var(--global-hover-color);
272
+ }
273
+
274
+ table[data-toggle="table"].af-table-enhanced tbody tr.af-row-selected {
275
+ background-color: color-mix(in srgb, var(--global-theme-color) 12%, transparent);
276
+ }
277
+
180
278
  // Table of Contents
181
279
 
182
- nav[data-toggle="toc"] {
280
+ #toc-sidebar {
281
+ z-index: 1;
183
282
  top: 5rem;
283
+ padding-left: 0.35rem;
284
+ border-left: 1px solid var(--global-divider-color);
285
+
286
+ .toc-list {
287
+ list-style: none;
288
+ margin: 0;
289
+ padding-left: 0;
290
+ }
184
291
 
185
- .nav .nav > li > a {
186
- font-size: 0.75rem;
292
+ .toc-list .toc-list {
293
+ border-left: 1px solid color-mix(in srgb, var(--global-divider-color) 85%, transparent);
294
+ margin-top: 0.15rem;
295
+ margin-left: 0.2rem;
296
+ padding-left: 0.75rem;
187
297
  }
188
298
 
189
- .nav > li > a {
299
+ .toc-link {
190
300
  color: var(--global-text-color);
191
- font-size: 0.75rem;
301
+ display: block;
302
+ font-size: 0.82rem;
303
+ line-height: 1.38;
304
+ margin: 0.12rem 0;
305
+ padding: 0.13rem 0.4rem 0.13rem 0.55rem;
306
+ border-left: 2px solid transparent;
307
+ transition:
308
+ border-color 0.2s ease,
309
+ color 0.2s ease,
310
+ transform 0.2s ease;
192
311
 
193
312
  &:hover {
194
313
  color: var(--global-hover-color);
195
314
  border-left-color: var(--global-hover-color);
315
+ text-decoration: none;
316
+ transform: translateX(2px);
196
317
  }
197
318
  }
198
319
 
199
- .nav-link.active {
320
+ .toc-link.is-active-link {
200
321
  color: var(--global-theme-color);
201
322
  border-left-color: var(--global-theme-color);
202
- font-size: 0.75rem;
323
+ font-weight: 600;
324
+ transform: translateX(3px);
203
325
 
204
326
  &:hover {
205
327
  color: var(--global-hover-color);
@@ -211,7 +333,7 @@ nav[data-toggle="toc"] {
211
333
  /* small screens */
212
334
  @media (max-width: 576px) {
213
335
  /* override stickyness so that the navigation does not follow scrolling */
214
- nav[data-toggle="toc"] {
336
+ #toc-sidebar {
215
337
  visibility: hidden;
216
338
  height: 0;
217
339
  top: 0;
@@ -446,10 +568,6 @@ nav[data-toggle="toc"] {
446
568
  z-index: 999;
447
569
  }
448
570
 
449
- #toc-sidebar {
450
- z-index: 1;
451
- }
452
-
453
571
  .echarts {
454
572
  height: 400px;
455
573
  width: 100%;
@@ -574,18 +692,58 @@ figure.cover {
574
692
  // Calendar
575
693
 
576
694
  .calendar-toggle-btn {
577
- background-color: #007acc;
578
- color: white;
579
- padding: 0.5em 1em;
580
- border: none;
581
- border-radius: 6px;
582
- font-size: 1em;
695
+ align-items: center;
696
+ background-color: transparent;
697
+ border: 1px solid var(--global-theme-color);
698
+ border-radius: 999px;
699
+ color: var(--global-theme-color);
583
700
  cursor: pointer;
584
- transition: background-color 0.3s;
701
+ display: inline-flex;
702
+ font-size: 0.92rem;
703
+ font-weight: 600;
704
+ gap: 0.25rem;
705
+ padding: 0.45rem 0.95rem;
706
+ transition:
707
+ background-color 0.2s ease,
708
+ color 0.2s ease,
709
+ box-shadow 0.2s ease,
710
+ transform 0.16s ease;
711
+ }
712
+
713
+ .calendar-toggle-btn:focus-visible {
714
+ outline: 2px solid color-mix(in srgb, var(--global-theme-color) 45%, transparent);
715
+ outline-offset: 2px;
585
716
  }
586
717
 
587
718
  .calendar-toggle-btn:hover {
588
- background-color: #005fa3;
719
+ background-color: color-mix(in srgb, var(--global-theme-color) 14%, transparent);
720
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.09);
721
+ color: var(--global-theme-color);
722
+ transform: translateY(-1px);
723
+ }
724
+
725
+ .calendar-toggle-btn:active {
726
+ transform: translateY(0);
727
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
728
+ }
729
+
730
+ .calendar-toggle-btn[aria-pressed="true"],
731
+ .calendar-toggle-btn.is-open {
732
+ background-color: var(--global-theme-color);
733
+ color: var(--global-hover-text-color);
734
+ border-color: var(--global-theme-color);
735
+ }
736
+
737
+ .calendar-toggle-btn[aria-pressed="true"]:hover,
738
+ .calendar-toggle-btn.is-open:hover {
739
+ background-color: var(--global-hover-color);
740
+ border-color: var(--global-hover-color);
741
+ color: var(--global-hover-text-color);
742
+ }
743
+
744
+ .calendar-toggle-btn.btn-sm {
745
+ cursor: pointer;
746
+ font-size: 0.88rem;
589
747
  }
590
748
 
591
749
  .google-calendar-embed {
@@ -2,9 +2,15 @@
2
2
  function toggleCalendar() {
3
3
  const el = document.getElementById("calendar-container");
4
4
  const btn = document.getElementById("calendar-toggle-btn");
5
+ if (!el || !btn) {
6
+ return;
7
+ }
8
+
5
9
  const isHidden = el.style.display === "none";
6
10
  el.style.display = isHidden ? "block" : "none";
7
11
  btn.innerText = isHidden ? "Hide Calendar" : "Show Calendar";
12
+ btn.setAttribute("aria-pressed", isHidden ? "true" : "false");
13
+ btn.classList.toggle("is-open", isHidden);
8
14
 
9
15
  // Update calendar URL when toggling to ensure correct theme
10
16
  if (isHidden) {
data/assets/js/common.js CHANGED
@@ -45,8 +45,8 @@ document.addEventListener("DOMContentLoaded", () => {
45
45
  });
46
46
 
47
47
  const tocSidebar = document.querySelector("#toc-sidebar");
48
+ const contentRoot = document.querySelector('[role="main"]') || document.querySelector("main") || document.body;
48
49
  const buildSidebarToc = (tocRoot) => {
49
- const contentRoot = document.querySelector("main") || document.body;
50
50
  const headings = Array.from(contentRoot.querySelectorAll("h2, h3")).filter((heading) => {
51
51
  return !heading.hasAttribute("data-toc-skip");
52
52
  });
@@ -56,7 +56,7 @@ document.addEventListener("DOMContentLoaded", () => {
56
56
  }
57
57
 
58
58
  const list = document.createElement("ul");
59
- list.className = "nav";
59
+ list.className = "toc-list";
60
60
 
61
61
  headings.forEach((heading) => {
62
62
  if (!heading.id) {
@@ -68,12 +68,13 @@ document.addEventListener("DOMContentLoaded", () => {
68
68
  }
69
69
 
70
70
  const item = document.createElement("li");
71
+ item.className = "toc-list-item";
71
72
  const link = document.createElement("a");
72
- link.className = "nav-link";
73
+ link.className = "toc-link";
73
74
  link.href = `#${heading.id}`;
74
- link.textContent = heading.textContent.trim();
75
+ link.textContent = heading.dataset.tocText || heading.textContent.trim();
75
76
  if (heading.tagName.toLowerCase() === "h3") {
76
- link.classList.add("ml-4");
77
+ item.classList.add("is-collapsible");
77
78
  }
78
79
 
79
80
  item.appendChild(link);
@@ -88,8 +89,51 @@ document.addEventListener("DOMContentLoaded", () => {
88
89
  heading.setAttribute("data-toc-skip", "");
89
90
  });
90
91
 
91
- if (window.Toc && typeof window.Toc.init === "function") {
92
- window.Toc.init(tocSidebar);
92
+ const headings = Array.from(contentRoot.querySelectorAll("h2, h3")).filter((heading) => !heading.hasAttribute("data-toc-skip"));
93
+ headings.forEach((heading) => {
94
+ if (!heading.id) {
95
+ heading.id = heading.textContent
96
+ .trim()
97
+ .toLowerCase()
98
+ .replace(/[^a-z0-9]+/g, "-")
99
+ .replace(/^-+|-+$/g, "");
100
+ }
101
+ });
102
+
103
+ const applyCustomTocLabels = () => {
104
+ tocSidebar.querySelectorAll(".toc-link").forEach((link) => {
105
+ const anchor = link.getAttribute("href") || "";
106
+ const headingId = decodeURIComponent(anchor.replace(/^#/, ""));
107
+ if (!headingId) {
108
+ return;
109
+ }
110
+ const heading = document.getElementById(headingId);
111
+ const customText = heading?.dataset?.tocText;
112
+ if (customText) {
113
+ link.textContent = customText;
114
+ }
115
+ });
116
+ };
117
+
118
+ if (window.tocbot && typeof window.tocbot.init === "function" && headings.length > 0) {
119
+ if (typeof window.tocbot.destroy === "function") {
120
+ window.tocbot.destroy();
121
+ }
122
+
123
+ window.tocbot.init({
124
+ tocSelector: "#toc-sidebar",
125
+ contentSelector: '[role="main"]',
126
+ headingSelector: "h2, h3",
127
+ ignoreSelector: "[data-toc-skip]",
128
+ hasInnerContainers: true,
129
+ collapseDepth: 6,
130
+ orderedList: false,
131
+ activeLinkClass: "is-active-link",
132
+ scrollSmooth: true,
133
+ scrollSmoothOffset: -80,
134
+ headingsOffset: 80,
135
+ });
136
+ applyCustomTocLabels();
93
137
  } else {
94
138
  buildSidebarToc(tocSidebar);
95
139
  }
@@ -1,18 +1,35 @@
1
- $(document).ready(function () {
2
- // Let external links in jupyter notebooks open in new tab
3
- let jupyterNotebooks = $(".jupyter-notebook-iframe-container");
4
- jupyterNotebooks.each(function () {
5
- let iframeBody = $(this).find("iframe").get(0).contentWindow.document.body;
6
- // Get all <a> elements in the bodyElement
7
- let links = $(iframeBody).find("a");
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const notebookIframes = document.querySelectorAll(".jupyter-notebook-iframe-container iframe");
8
3
 
9
- // Loop through each <a> element
10
- links.each(function () {
11
- // Check if the <a> element has an 'href' attribute
12
- if ($(this).attr("href")) {
13
- // Set the 'target' attribute to '_blank' to open the link in a new tab/window
14
- $(this).attr("target", "_blank");
4
+ const updateNotebookLinks = (iframe) => {
5
+ try {
6
+ const iframeDocument = iframe.contentDocument || iframe.contentWindow?.document;
7
+ if (!iframeDocument || !iframeDocument.body) {
8
+ return;
15
9
  }
16
- });
10
+
11
+ iframeDocument.querySelectorAll("a[href]").forEach((link) => {
12
+ link.setAttribute("target", "_blank");
13
+ const currentRel = link.getAttribute("rel") || "";
14
+ const relParts = currentRel.split(/\s+/).filter(Boolean);
15
+ if (!relParts.includes("noopener")) {
16
+ relParts.push("noopener");
17
+ }
18
+ if (!relParts.includes("noreferrer")) {
19
+ relParts.push("noreferrer");
20
+ }
21
+ link.setAttribute("rel", relParts.join(" "));
22
+ });
23
+ } catch (_error) {
24
+ // Cross-origin iframe access is blocked by design.
25
+ }
26
+ };
27
+
28
+ notebookIframes.forEach((iframe) => {
29
+ if (iframe.contentDocument?.readyState === "complete") {
30
+ updateNotebookLinks(iframe);
31
+ }
32
+
33
+ iframe.addEventListener("load", () => updateNotebookLinks(iframe));
17
34
  });
18
35
  });
data/assets/js/masonry.js CHANGED
@@ -1,12 +1,22 @@
1
- $(document).ready(function () {
2
- // Init Masonry
3
- var $grid = $(".grid").masonry({
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const grid = document.querySelector(".grid");
3
+ if (!grid || typeof window.Masonry !== "function") {
4
+ return;
5
+ }
6
+
7
+ const masonry = new window.Masonry(grid, {
4
8
  gutter: 10,
5
9
  horizontalOrder: true,
6
10
  itemSelector: ".grid-item",
7
11
  });
8
- // Layout Masonry after each image loads
9
- $grid.imagesLoaded().progress(function () {
10
- $grid.masonry("layout");
11
- });
12
+
13
+ if (typeof window.imagesLoaded === "function") {
14
+ const tracker = window.imagesLoaded(grid);
15
+ if (tracker && typeof tracker.on === "function") {
16
+ tracker.on("progress", () => masonry.layout());
17
+ return;
18
+ }
19
+ }
20
+
21
+ masonry.layout();
12
22
  });
@@ -0,0 +1,328 @@
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ if (window.alFolio?.compatBootstrap) {
3
+ return;
4
+ }
5
+
6
+ const asBool = (value) => String(value || "").toLowerCase() === "true";
7
+
8
+ const parseSortValue = (value) => {
9
+ if (typeof value === "number") {
10
+ return { type: "number", value };
11
+ }
12
+
13
+ const text = String(value ?? "").trim();
14
+ const numeric = Number(text.replace(/[$,%\s,]/g, ""));
15
+ if (text !== "" && Number.isFinite(numeric) && /^[-+]?[$]?\d/.test(text)) {
16
+ return { type: "number", value: numeric };
17
+ }
18
+
19
+ return { type: "string", value: text.toLowerCase() };
20
+ };
21
+
22
+ class AfTableEngine {
23
+ constructor(table) {
24
+ this.table = table;
25
+ this.columns = Array.from(table.querySelectorAll("thead th")).map((th, index) => ({
26
+ element: th,
27
+ index,
28
+ field: th.dataset.field || `__col_${index}`,
29
+ checkbox: asBool(th.dataset.checkbox),
30
+ sortable: asBool(th.dataset.sortable),
31
+ align: th.dataset.align || "",
32
+ halign: th.dataset.halign || th.dataset.align || "",
33
+ }));
34
+ this.state = {
35
+ allRows: [],
36
+ filteredRows: [],
37
+ search: "",
38
+ page: 1,
39
+ pageSize: Number.parseInt(table.dataset.pageSize || "10", 10) || 10,
40
+ sortField: null,
41
+ sortDirection: "asc",
42
+ };
43
+ this.options = {
44
+ search: asBool(table.dataset.search),
45
+ pagination: asBool(table.dataset.pagination),
46
+ clickToSelect: asBool(table.dataset.clickToSelect),
47
+ dataUrl: table.dataset.url || "",
48
+ };
49
+ this.tbody = table.querySelector("tbody") || table.createTBody();
50
+ }
51
+
52
+ async init() {
53
+ this.table.classList.add("table", "table-hover", "af-table-enhanced");
54
+ this.buildShell();
55
+ this.bindHeaderSort();
56
+ this.state.allRows = await this.loadRows();
57
+ this.applyFiltersAndSort();
58
+ this.render();
59
+ }
60
+
61
+ buildShell() {
62
+ const shell = document.createElement("div");
63
+ shell.className = "af-table-shell";
64
+
65
+ this.table.parentNode.insertBefore(shell, this.table);
66
+ shell.appendChild(this.table);
67
+
68
+ const toolbar = document.createElement("div");
69
+ toolbar.className = "af-table-toolbar";
70
+ shell.insertBefore(toolbar, this.table);
71
+ this.toolbar = toolbar;
72
+
73
+ if (this.options.search) {
74
+ const searchInput = document.createElement("input");
75
+ searchInput.className = "af-table-search";
76
+ searchInput.type = "search";
77
+ searchInput.placeholder = "Search table...";
78
+ searchInput.setAttribute("aria-label", "Search table rows");
79
+ searchInput.addEventListener("input", () => {
80
+ this.state.search = searchInput.value.trim().toLowerCase();
81
+ this.state.page = 1;
82
+ this.applyFiltersAndSort();
83
+ this.render();
84
+ });
85
+ toolbar.appendChild(searchInput);
86
+ }
87
+
88
+ if (this.options.pagination) {
89
+ const pagination = document.createElement("div");
90
+ pagination.className = "af-table-pagination";
91
+
92
+ const info = document.createElement("span");
93
+ info.className = "af-table-page-info";
94
+ pagination.appendChild(info);
95
+ this.pageInfo = info;
96
+
97
+ const prevButton = document.createElement("button");
98
+ prevButton.type = "button";
99
+ prevButton.textContent = "Prev";
100
+ prevButton.addEventListener("click", () => {
101
+ if (this.state.page > 1) {
102
+ this.state.page -= 1;
103
+ this.render();
104
+ }
105
+ });
106
+ pagination.appendChild(prevButton);
107
+ this.prevButton = prevButton;
108
+
109
+ const nextButton = document.createElement("button");
110
+ nextButton.type = "button";
111
+ nextButton.textContent = "Next";
112
+ nextButton.addEventListener("click", () => {
113
+ if (this.state.page < this.totalPages()) {
114
+ this.state.page += 1;
115
+ this.render();
116
+ }
117
+ });
118
+ pagination.appendChild(nextButton);
119
+ this.nextButton = nextButton;
120
+
121
+ this.toolbar.appendChild(pagination);
122
+ }
123
+ }
124
+
125
+ bindHeaderSort() {
126
+ this.columns.forEach((column) => {
127
+ if (!column.sortable || column.checkbox) {
128
+ return;
129
+ }
130
+
131
+ if (column.halign) {
132
+ column.element.style.textAlign = column.halign;
133
+ }
134
+ column.element.classList.add("af-sortable");
135
+ column.element.addEventListener("click", () => {
136
+ if (this.state.sortField === column.field) {
137
+ this.state.sortDirection = this.state.sortDirection === "asc" ? "desc" : "asc";
138
+ } else {
139
+ this.state.sortField = column.field;
140
+ this.state.sortDirection = "asc";
141
+ }
142
+ this.renderHeaders();
143
+ this.applyFiltersAndSort();
144
+ this.render();
145
+ });
146
+ });
147
+ }
148
+
149
+ async loadRows() {
150
+ if (!this.options.dataUrl) {
151
+ return this.readRowsFromMarkup();
152
+ }
153
+
154
+ try {
155
+ const response = await fetch(this.options.dataUrl, { credentials: "same-origin" });
156
+ if (!response.ok) {
157
+ return this.readRowsFromMarkup();
158
+ }
159
+ const payload = await response.json();
160
+ if (!Array.isArray(payload)) {
161
+ return this.readRowsFromMarkup();
162
+ }
163
+ return payload.map((row) => this.normalizeRow(row));
164
+ } catch (_error) {
165
+ return this.readRowsFromMarkup();
166
+ }
167
+ }
168
+
169
+ readRowsFromMarkup() {
170
+ const rows = Array.from(this.tbody.querySelectorAll("tr"));
171
+ return rows.map((tr) => {
172
+ const cells = Array.from(tr.querySelectorAll("td"));
173
+ const row = {};
174
+ this.columns.forEach((column) => {
175
+ const cell = cells[column.index];
176
+ if (column.checkbox) {
177
+ row[column.field] = Boolean(cell?.querySelector('input[type="checkbox"]')?.checked);
178
+ } else {
179
+ row[column.field] = cell ? cell.textContent.trim() : "";
180
+ }
181
+ });
182
+ return row;
183
+ });
184
+ }
185
+
186
+ normalizeRow(row) {
187
+ const normalized = {};
188
+ this.columns.forEach((column) => {
189
+ if (column.checkbox) {
190
+ normalized[column.field] = Boolean(row[column.field]);
191
+ return;
192
+ }
193
+ normalized[column.field] = row[column.field] ?? "";
194
+ });
195
+ return normalized;
196
+ }
197
+
198
+ applyFiltersAndSort() {
199
+ let rows = this.state.allRows.slice();
200
+ if (this.state.search) {
201
+ rows = rows.filter((row) => {
202
+ return this.columns.some((column) => {
203
+ if (column.checkbox) {
204
+ return false;
205
+ }
206
+ const value = row[column.field];
207
+ return String(value ?? "")
208
+ .toLowerCase()
209
+ .includes(this.state.search);
210
+ });
211
+ });
212
+ }
213
+
214
+ if (this.state.sortField) {
215
+ const direction = this.state.sortDirection === "asc" ? 1 : -1;
216
+ const field = this.state.sortField;
217
+ rows.sort((leftRow, rightRow) => {
218
+ const left = parseSortValue(leftRow[field]);
219
+ const right = parseSortValue(rightRow[field]);
220
+ if (left.type === "number" && right.type === "number") {
221
+ return (left.value - right.value) * direction;
222
+ }
223
+ return String(left.value).localeCompare(String(right.value)) * direction;
224
+ });
225
+ }
226
+
227
+ this.state.filteredRows = rows;
228
+ if (this.state.page > this.totalPages()) {
229
+ this.state.page = this.totalPages();
230
+ }
231
+ }
232
+
233
+ totalPages() {
234
+ if (!this.options.pagination) {
235
+ return 1;
236
+ }
237
+ return Math.max(1, Math.ceil(this.state.filteredRows.length / this.state.pageSize));
238
+ }
239
+
240
+ paginatedRows() {
241
+ if (!this.options.pagination) {
242
+ return this.state.filteredRows;
243
+ }
244
+ const from = (this.state.page - 1) * this.state.pageSize;
245
+ return this.state.filteredRows.slice(from, from + this.state.pageSize);
246
+ }
247
+
248
+ renderHeaders() {
249
+ this.columns.forEach((column) => {
250
+ const label = column.element.dataset.afLabel || column.element.textContent.trim();
251
+ column.element.dataset.afLabel = label;
252
+ if (!column.sortable || column.checkbox) {
253
+ return;
254
+ }
255
+
256
+ if (this.state.sortField === column.field) {
257
+ const directionMarker = this.state.sortDirection === "asc" ? " \u2191" : " \u2193";
258
+ column.element.textContent = `${label}${directionMarker}`;
259
+ } else {
260
+ column.element.textContent = label;
261
+ }
262
+ });
263
+ }
264
+
265
+ render() {
266
+ this.renderHeaders();
267
+ this.tbody.replaceChildren();
268
+
269
+ this.paginatedRows().forEach((row) => {
270
+ const tr = document.createElement("tr");
271
+ let checkboxControl = null;
272
+
273
+ this.columns.forEach((column) => {
274
+ const td = document.createElement("td");
275
+ if (column.align) {
276
+ td.style.textAlign = column.align;
277
+ }
278
+
279
+ if (column.checkbox) {
280
+ const checkbox = document.createElement("input");
281
+ checkbox.type = "checkbox";
282
+ checkbox.checked = Boolean(row[column.field]);
283
+ checkbox.setAttribute("aria-label", "Select row");
284
+ checkbox.addEventListener("change", () => {
285
+ row[column.field] = checkbox.checked;
286
+ tr.classList.toggle("af-row-selected", checkbox.checked);
287
+ });
288
+ checkboxControl = checkbox;
289
+ td.appendChild(checkbox);
290
+ tr.classList.toggle("af-row-selected", checkbox.checked);
291
+ } else {
292
+ td.textContent = String(row[column.field] ?? "");
293
+ }
294
+
295
+ tr.appendChild(td);
296
+ });
297
+
298
+ if (this.options.clickToSelect && checkboxControl) {
299
+ tr.addEventListener("click", (event) => {
300
+ if (event.target.closest('a, button, input, label, select, textarea')) {
301
+ return;
302
+ }
303
+ checkboxControl.checked = !checkboxControl.checked;
304
+ checkboxControl.dispatchEvent(new Event("change", { bubbles: true }));
305
+ });
306
+ }
307
+
308
+ this.tbody.appendChild(tr);
309
+ });
310
+
311
+ if (this.options.pagination && this.pageInfo && this.prevButton && this.nextButton) {
312
+ const totalPages = this.totalPages();
313
+ this.pageInfo.textContent = `Page ${this.state.page} / ${totalPages}`;
314
+ this.prevButton.disabled = this.state.page <= 1;
315
+ this.nextButton.disabled = this.state.page >= totalPages;
316
+ }
317
+ }
318
+ }
319
+
320
+ document.querySelectorAll('table[data-toggle="table"]').forEach((table) => {
321
+ if (table.dataset.afTableInitialized === "true") {
322
+ return;
323
+ }
324
+ table.dataset.afTableInitialized = "true";
325
+ const engine = new AfTableEngine(table);
326
+ engine.init();
327
+ });
328
+ });
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AlFolioCore
4
- VERSION = "1.0.3"
4
+ VERSION = "1.0.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: al_folio_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - al-folio maintainers
@@ -179,6 +179,7 @@ files:
179
179
  - assets/js/nav-toggle.js
180
180
  - assets/js/no_defer.js
181
181
  - assets/js/progress-bar.js
182
+ - assets/js/table-engine.js
182
183
  - assets/js/tabs.js
183
184
  - assets/js/theme.js
184
185
  - assets/js/tooltips-setup.js