jekyll-theme-zer0 1.30.0 → 1.31.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +171 -0
  3. data/_data/backlog.yml +30 -7
  4. data/_data/consumers.yml +99 -0
  5. data/_data/features.yml +1 -1
  6. data/_data/theme-manifest.yml +35 -33
  7. data/_data/ui-text.yml +1 -0
  8. data/_includes/README.md +1 -1
  9. data/_includes/analytics/posthog.html +28 -9
  10. data/_includes/components/README.md +1 -1
  11. data/_includes/components/background-image.html +7 -1
  12. data/_includes/components/cookie-consent.html +30 -30
  13. data/_includes/components/mermaid.html +14 -7
  14. data/_includes/components/preview-image.html +22 -3
  15. data/_includes/components/theme-preview-gallery.html +1 -1
  16. data/_includes/content/intro.html +7 -0
  17. data/_includes/content/seo.html +8 -0
  18. data/_includes/content/sitemap.html +220 -263
  19. data/_includes/core/branding.html +8 -15
  20. data/_includes/core/head.html +29 -5
  21. data/_includes/core/header.html +14 -2
  22. data/_includes/core/tokens-inline.html +16 -1
  23. data/_includes/navigation/navbar.html +9 -6
  24. data/_includes/setup/wizard.html +5 -1
  25. data/_layouts/article.html +15 -3
  26. data/_layouts/home.html +13 -1
  27. data/_layouts/note.html +2 -1
  28. data/_layouts/notebook.html +2 -1
  29. data/_layouts/root.html +22 -0
  30. data/_sass/components/_cookie-banner.scss +48 -0
  31. data/_sass/components/_setup-wizard.scss +19 -1
  32. data/_sass/components/_ui-enhancements.scss +11 -0
  33. data/_sass/core/_navbar.scss +137 -70
  34. data/_sass/core/_sidebar-extras.scss +2 -14
  35. data/_sass/core/code-copy.scss +27 -8
  36. data/assets/css/extension-points-demo.css +27 -0
  37. data/assets/data/wiki-index.json +19 -0
  38. data/assets/js/auto-hide-nav.js +53 -13
  39. data/assets/js/code-copy.js +72 -1
  40. data/assets/js/extension-points-demo.js +48 -0
  41. data/assets/js/mermaid-diagrams.js +128 -22
  42. data/assets/js/obsidian-local-graph.js +27 -5
  43. data/assets/js/setup-wizard.js +7 -2
  44. data/scripts/ci/test_visual_evidence_autogen.py +64 -2
  45. data/scripts/ci/visual_evidence_autogen.py +14 -4
  46. data/scripts/features/install-preview-generator +1 -1
  47. data/scripts/lib/README.md +12 -0
  48. data/scripts/lib/preview_generator.py +2 -2
  49. metadata +4 -2
@@ -146,7 +146,7 @@
146
146
  <div class="row mt-3">
147
147
  <div class="col-12">
148
148
  <div class="d-flex justify-content-between align-items-center">
149
- <small class="text-muted">
149
+ <small class="text-muted" aria-live="polite">
150
150
  Showing <span id="visibleCount" class="fw-bold">0</span> of
151
151
  <span id="totalCount" class="fw-bold">0</span> pages
152
152
  </small>
@@ -225,7 +225,7 @@
225
225
  data-url="{{ page.url | relative_url }}">
226
226
 
227
227
  <td>
228
- <span class="badge bg-primary rounded-pill">Pages</span>
228
+ <span class="badge text-bg-primary rounded-pill">Pages</span>
229
229
  </td>
230
230
  <td>
231
231
  <a href="{{ page.url | relative_url }}"
@@ -252,7 +252,7 @@
252
252
  <span class="badge bg-secondary me-1">{{ category }}</span>
253
253
  {% endfor %}
254
254
  {% if page.categories.size > 2 %}
255
- <span class="badge">+{{ page.categories.size | minus: 2 }}</span>
255
+ <span class="badge text-body-secondary border">+{{ page.categories.size | minus: 2 }}</span>
256
256
  {% endif %}
257
257
  {% endif %}
258
258
  </td>
@@ -262,7 +262,7 @@
262
262
  <span class="badge bg-outline-primary me-1">#{{ tag }}</span>
263
263
  {% endfor %}
264
264
  {% if page.tags.size > 3 %}
265
- <span class="badge">+{{ page.tags.size | minus: 3 }}</span>
265
+ <span class="badge text-body-secondary border">+{{ page.tags.size | minus: 3 }}</span>
266
266
  {% endif %}
267
267
  {% endif %}
268
268
  </td>
@@ -298,7 +298,7 @@
298
298
  data-url="{{ item.url | relative_url }}">
299
299
 
300
300
  <td>
301
- <span class="badge bg-info rounded-pill">{{ collection.label | capitalize }}</span>
301
+ <span class="badge text-bg-info rounded-pill">{{ collection.label | capitalize }}</span>
302
302
  </td>
303
303
  <td>
304
304
  <a href="{{ item.url | relative_url }}"
@@ -325,7 +325,7 @@
325
325
  <span class="badge bg-secondary me-1">{{ category }}</span>
326
326
  {% endfor %}
327
327
  {% if item.categories.size > 2 %}
328
- <span class="badge">+{{ item.categories.size | minus: 2 }}</span>
328
+ <span class="badge text-body-secondary border">+{{ item.categories.size | minus: 2 }}</span>
329
329
  {% endif %}
330
330
  {% endif %}
331
331
  </td>
@@ -335,7 +335,7 @@
335
335
  <span class="badge bg-outline-primary me-1">#{{ tag }}</span>
336
336
  {% endfor %}
337
337
  {% if item.tags.size > 3 %}
338
- <span class="badge">+{{ item.tags.size | minus: 3 }}</span>
338
+ <span class="badge text-body-secondary border">+{{ item.tags.size | minus: 3 }}</span>
339
339
  {% endif %}
340
340
  {% endif %}
341
341
  </td>
@@ -376,6 +376,11 @@
376
376
  </div>
377
377
  </div>
378
378
 
379
+ {% comment %} Next page of results (both views); shown by the script only when more matches remain {% endcomment %}
380
+ <div class="text-center mt-3 d-none" id="sitemapShowMore">
381
+ <button type="button" class="btn btn-outline-primary" id="sitemapShowMoreBtn">Show more</button>
382
+ </div>
383
+
379
384
  {% comment %} ================================ {% endcomment %}
380
385
  {% comment %} ENHANCED SITEMAP FUNCTIONALITY {% endcomment %}
381
386
  {% comment %} ================================ {% endcomment %}
@@ -404,9 +409,11 @@ th:hover .sort-icon {
404
409
  transition: background-color 0.15s ease;
405
410
  }
406
411
 
412
+ /* Outline tag chip: emphasis text (light #052c65 / dark #6ea8fe) instead of
413
+ #0d6efd, which is 3.97:1 on the light table stripe */
407
414
  .badge.bg-outline-primary {
408
- color: #0d6efd;
409
- border: 1px solid #0d6efd;
415
+ color: var(--bs-primary-text-emphasis);
416
+ border: 1px solid var(--bs-primary-border-subtle);
410
417
  background-color: transparent;
411
418
  }
412
419
 
@@ -466,8 +473,18 @@ th:hover .sort-icon {
466
473
  /**
467
474
  * Enhanced Sitemap Functionality
468
475
  * Features: Advanced search, sorting, filtering, statistics, and card/table views
476
+ *
477
+ * Paginated: every row is still rendered server-side (so the index works
478
+ * without JS and with ?q= from the search modal's "View all results"), but
479
+ * only PAGE_SIZE matching rows stay in the document; the rest are kept in
480
+ * memory and a "Show more" button appends the next page. Cards are built on
481
+ * the first switch to card view, one page at a time. On it-journey.dev the
482
+ * index (431 rows plus 431 eagerly built cards) put ~19,900 elements on
483
+ * /search/ before the visitor typed anything.
469
484
  */
470
485
  class SitemapManager {
486
+ static PAGE_SIZE = 50;
487
+
471
488
  constructor() {
472
489
  this.searchInput = document.getElementById('searchBar');
473
490
  this.collectionFilter = document.getElementById('collectionFilter');
@@ -483,27 +500,30 @@ class SitemapManager {
483
500
  this.cardsContainer = document.getElementById('sitemapCardsContainer');
484
501
  this.tableViewBtn = document.getElementById('tableView');
485
502
  this.cardViewBtn = document.getElementById('cardView');
486
-
503
+ this.showMoreWrap = document.getElementById('sitemapShowMore');
504
+ this.showMoreBtn = document.getElementById('sitemapShowMoreBtn');
505
+
487
506
  this.rows = Array.from(this.tbody.querySelectorAll('tr'));
488
507
  this.visibleCount = document.getElementById('visibleCount');
489
508
  this.totalCount = document.getElementById('totalCount');
490
-
509
+
491
510
  this.searchTimeout = null;
492
511
  this.collections = new Set();
493
512
  this.currentView = 'table';
494
-
513
+ this.matches = this.rows;
514
+ this.limit = SitemapManager.PAGE_SIZE;
515
+ this.cards = new WeakMap(); // row -> card element, built on demand
516
+
495
517
  this.init();
496
518
  }
497
-
519
+
498
520
  init() {
499
521
  this.setupCollectionFilter();
500
- this.updateCounts();
501
522
  this.updateStatistics();
502
- this.generateCards();
503
523
  this.bindEvents();
504
- this.handleUrlParams();
524
+ if (!this.handleUrlParams()) this.refresh();
505
525
  }
506
-
526
+
507
527
  setupCollectionFilter() {
508
528
  // Collect unique collections
509
529
  this.rows.forEach(row => {
@@ -512,7 +532,7 @@ class SitemapManager {
512
532
  this.collections.add(collection);
513
533
  }
514
534
  });
515
-
535
+
516
536
  // Populate collection filter
517
537
  Array.from(this.collections).sort().forEach(collection => {
518
538
  const option = document.createElement('option');
@@ -521,324 +541,265 @@ class SitemapManager {
521
541
  this.collectionFilter.appendChild(option);
522
542
  });
523
543
  }
524
-
525
- generateCards() {
526
- this.cardsContainer.innerHTML = '';
527
-
528
- this.rows.forEach(row => {
529
- const collection = row.dataset.collection;
530
- const title = row.dataset.title;
531
- const description = row.dataset.description || 'No description available';
532
- const categories = row.dataset.categories;
533
- const tags = row.dataset.tags;
534
- const date = row.dataset.date;
535
- const url = row.dataset.url;
536
-
537
- const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
538
- year: 'numeric',
539
- month: 'short',
540
- day: 'numeric'
541
- }) : 'Unknown';
542
-
543
- const badgeClass = collection === 'pages' ? 'bg-primary' : 'bg-info';
544
-
545
- const cardHtml = `
546
- <div class="col sitemap-card"
547
- data-collection="${collection}"
548
- data-title="${title}"
549
- data-description="${description}"
550
- data-categories="${categories}"
551
- data-tags="${tags}"
552
- data-date="${date}">
553
- <div class="card h-100 shadow-sm">
554
- <div class="card-header d-flex justify-content-between align-items-center">
555
- <span class="badge ${badgeClass} rounded-pill">${collection.charAt(0).toUpperCase() + collection.slice(1)}</span>
556
- <small class="text-muted">${formattedDate}</small>
557
- </div>
558
- <div class="card-body">
559
- <h6 class="card-title">
560
- <a href="${url}" class="text-decoration-none">${title}</a>
561
- </h6>
562
- <p class="card-text text-muted small">${description.length > 100 ? description.substring(0, 100) + '...' : description}</p>
563
- </div>
564
- <div class="card-footer bg-transparent">
565
- <div class="d-flex justify-content-between align-items-center">
566
- <div class="btn-group btn-group-sm" role="group">
567
- <a href="${url}" class="btn btn-outline-primary btn-sm" title="View page">
568
- <i class="bi bi-eye"></i>
569
- </a>
570
- <button class="btn btn-outline-secondary btn-sm copy-url"
571
- data-url="${location.origin}${url}" title="Copy URL">
572
- <i class="bi bi-clipboard"></i>
573
- </button>
574
- </div>
575
- ${categories ? `<small class="text-muted">${categories.split(',').slice(0,2).join(', ')}</small>` : ''}
576
- </div>
544
+
545
+ // Build (once) the card for a row. Text goes in through textContent /
546
+ // setAttribute, so a title or description containing quotes or markup
547
+ // can no longer break the card's HTML.
548
+ cardFor(row) {
549
+ let card = this.cards.get(row);
550
+ if (card) return card;
551
+
552
+ const d = row.dataset;
553
+ const collection = d.collection || '';
554
+ const description = d.description || 'No description available';
555
+ const formattedDate = d.date ? new Date(d.date).toLocaleDateString('en-US', {
556
+ year: 'numeric',
557
+ month: 'short',
558
+ day: 'numeric'
559
+ }) : 'Unknown';
560
+ const badgeClass = collection === 'pages' ? 'text-bg-primary' : 'text-bg-info';
561
+
562
+ card = document.createElement('div');
563
+ card.className = 'col sitemap-card';
564
+ card.innerHTML = `
565
+ <div class="card h-100 shadow-sm">
566
+ <div class="card-header d-flex justify-content-between align-items-center">
567
+ <span class="badge ${badgeClass} rounded-pill"></span>
568
+ <small class="text-muted"></small>
569
+ </div>
570
+ <div class="card-body">
571
+ <h3 class="card-title h6"><a class="text-decoration-none"></a></h3>
572
+ <p class="card-text text-muted small"></p>
573
+ </div>
574
+ <div class="card-footer bg-transparent">
575
+ <div class="d-flex justify-content-between align-items-center">
576
+ <div class="btn-group btn-group-sm" role="group">
577
+ <a class="btn btn-outline-primary btn-sm" title="View page" aria-label="View page">
578
+ <i class="bi bi-eye" aria-hidden="true"></i>
579
+ </a>
580
+ <button type="button" class="btn btn-outline-secondary btn-sm copy-url" title="Copy URL" aria-label="Copy URL">
581
+ <i class="bi bi-clipboard" aria-hidden="true"></i>
582
+ </button>
577
583
  </div>
584
+ <small class="text-muted sitemap-card-categories"></small>
578
585
  </div>
579
586
  </div>
580
- `;
581
-
582
- this.cardsContainer.insertAdjacentHTML('beforeend', cardHtml);
583
- });
584
-
585
- // Re-attach copy URL event listeners for cards
586
- this.cardsContainer.querySelectorAll('.copy-url').forEach(btn => {
587
- btn.addEventListener('click', (e) => this.copyUrl(e));
588
- });
587
+ </div>`;
588
+ card.querySelector('.badge').textContent = collection.charAt(0).toUpperCase() + collection.slice(1);
589
+ card.querySelector('.card-header small').textContent = formattedDate;
590
+ const link = card.querySelector('.card-title a');
591
+ link.textContent = d.title || '';
592
+ link.href = d.url;
593
+ card.querySelector('.card-text').textContent = description.length > 100 ? description.substring(0, 100) + '...' : description;
594
+ card.querySelector('.card-footer a').href = d.url;
595
+ const copy = card.querySelector('.copy-url');
596
+ copy.dataset.url = location.origin + d.url;
597
+ copy.addEventListener('click', (e) => this.copyUrl(e));
598
+ card.querySelector('.sitemap-card-categories').textContent =
599
+ d.categories ? d.categories.split(',').slice(0, 2).join(', ') : '';
600
+
601
+ this.cards.set(row, card);
602
+ return card;
589
603
  }
590
-
604
+
591
605
  bindEvents() {
592
606
  // Search with debouncing
593
607
  this.searchInput.addEventListener('input', () => {
594
608
  clearTimeout(this.searchTimeout);
595
609
  this.searchTimeout = setTimeout(() => this.performSearch(), 300);
596
610
  });
597
-
611
+
598
612
  this.clearSearchBtn.addEventListener('click', () => {
599
613
  this.searchInput.value = '';
600
614
  this.performSearch();
601
615
  this.searchInput.focus();
602
616
  });
603
-
617
+
604
618
  // Filters
605
619
  this.collectionFilter.addEventListener('change', () => this.applyFilters());
606
620
  this.dateFilter.addEventListener('change', () => this.applyFilters());
607
-
621
+
608
622
  // Reset filters
609
623
  this.resetFiltersBtn.addEventListener('click', () => this.resetFilters());
610
-
624
+
611
625
  // Toggle statistics
612
626
  this.toggleStatsBtn.addEventListener('click', () => this.toggleStatistics());
613
-
627
+
614
628
  // View mode toggle
615
629
  this.tableViewBtn.addEventListener('change', () => {
616
630
  if (this.tableViewBtn.checked) {
617
631
  this.switchToTableView();
618
632
  }
619
633
  });
620
-
634
+
621
635
  this.cardViewBtn.addEventListener('change', () => {
622
636
  if (this.cardViewBtn.checked) {
623
637
  this.switchToCardView();
624
638
  }
625
639
  });
626
-
640
+
641
+ // Next page of results
642
+ if (this.showMoreBtn) {
643
+ this.showMoreBtn.addEventListener('click', () => {
644
+ this.limit += SitemapManager.PAGE_SIZE;
645
+ this.render();
646
+ });
647
+ }
648
+
627
649
  // Column sorting (table view only)
628
650
  this.table.querySelectorAll('th[data-type]').forEach(header => {
629
651
  header.addEventListener('click', () => this.sortTable(header));
630
652
  });
631
-
632
- // Copy URL functionality
633
- document.querySelectorAll('.copy-url').forEach(btn => {
634
- btn.addEventListener('click', (e) => this.copyUrl(e));
653
+
654
+ // Copy URL functionality (table rows; cards bind their own)
655
+ this.rows.forEach(row => {
656
+ row.querySelectorAll('.copy-url').forEach(btn => {
657
+ btn.addEventListener('click', (e) => this.copyUrl(e));
658
+ });
635
659
  });
636
-
660
+
637
661
  // Keyboard shortcuts
638
662
  document.addEventListener('keydown', (e) => this.handleKeyboardShortcuts(e));
639
663
  }
640
-
664
+
641
665
  switchToTableView() {
642
666
  this.currentView = 'table';
643
667
  this.tableViewContainer.classList.remove('d-none');
644
668
  this.cardViewContainer.classList.add('d-none');
669
+ this.render();
645
670
  }
646
-
671
+
647
672
  switchToCardView() {
648
673
  this.currentView = 'card';
649
674
  this.tableViewContainer.classList.add('d-none');
650
675
  this.cardViewContainer.classList.remove('d-none');
676
+ this.render();
651
677
  }
652
-
678
+
653
679
  performSearch() {
654
- const searchTerm = this.searchInput.value.toLowerCase().trim();
655
-
656
- if (this.currentView === 'table') {
657
- this.rows.forEach(row => {
658
- if (!searchTerm) {
659
- row.style.display = '';
660
- return;
661
- }
662
-
663
- const searchableText = [
664
- row.dataset.title,
665
- row.dataset.description,
666
- row.dataset.categories,
667
- row.dataset.tags,
668
- row.dataset.collection
669
- ].join(' ').toLowerCase();
670
-
671
- row.style.display = searchableText.includes(searchTerm) ? '' : 'none';
672
- });
673
- } else {
674
- // Card view search
675
- const cards = this.cardsContainer.querySelectorAll('.sitemap-card');
676
- cards.forEach(card => {
677
- if (!searchTerm) {
678
- card.style.display = '';
679
- return;
680
- }
681
-
682
- const searchableText = [
683
- card.dataset.title,
684
- card.dataset.description,
685
- card.dataset.categories,
686
- card.dataset.tags,
687
- card.dataset.collection
688
- ].join(' ').toLowerCase();
689
-
690
- card.style.display = searchableText.includes(searchTerm) ? '' : 'none';
691
- });
692
- }
693
-
694
680
  this.applyFilters();
695
681
  }
696
-
682
+
683
+ rowMatches(row, searchTerm, collectionFilter, dateFilter) {
684
+ if (searchTerm) {
685
+ const searchableText = [
686
+ row.dataset.title,
687
+ row.dataset.description,
688
+ row.dataset.categories,
689
+ row.dataset.tags,
690
+ row.dataset.collection
691
+ ].join(' ').toLowerCase();
692
+ if (!searchableText.includes(searchTerm)) return false;
693
+ }
694
+
695
+ if (collectionFilter && row.dataset.collection !== collectionFilter) return false;
696
+
697
+ if (dateFilter) {
698
+ const rowDate = new Date(row.dataset.date);
699
+ switch (dateFilter) {
700
+ case 'today':
701
+ return this.isSameDay(rowDate, new Date());
702
+ case 'week':
703
+ return this.isWithinDays(rowDate, 7);
704
+ case 'month':
705
+ return this.isWithinDays(rowDate, 30);
706
+ case 'year':
707
+ return this.isWithinDays(rowDate, 365);
708
+ }
709
+ }
710
+ return true;
711
+ }
712
+
713
+ // Recompute the matching rows from search + filters, back to page one.
697
714
  applyFilters() {
715
+ const searchTerm = this.searchInput.value.toLowerCase().trim();
698
716
  const collectionFilter = this.collectionFilter.value;
699
717
  const dateFilter = this.dateFilter.value;
700
-
718
+ this.matches = this.rows.filter(row => this.rowMatches(row, searchTerm, collectionFilter, dateFilter));
719
+ this.limit = SitemapManager.PAGE_SIZE;
720
+ this.render();
721
+ }
722
+
723
+ refresh() {
724
+ this.applyFilters();
725
+ }
726
+
727
+ // Put the first `limit` matches (in the current sort order) into the
728
+ // active view; everything else stays out of the document.
729
+ render() {
730
+ const shown = this.matches.slice(0, this.limit);
701
731
  if (this.currentView === 'table') {
702
- this.rows.forEach(row => {
703
- let show = true;
704
-
705
- // Skip if already hidden by search
706
- if (row.style.display === 'none') return;
707
-
708
- // Collection filter
709
- if (collectionFilter && row.dataset.collection !== collectionFilter) {
710
- show = false;
711
- }
712
-
713
- // Date filter
714
- if (dateFilter && show) {
715
- const rowDate = new Date(row.dataset.date);
716
- const now = new Date();
717
-
718
- switch (dateFilter) {
719
- case 'today':
720
- show = this.isSameDay(rowDate, now);
721
- break;
722
- case 'week':
723
- show = this.isWithinDays(rowDate, 7);
724
- break;
725
- case 'month':
726
- show = this.isWithinDays(rowDate, 30);
727
- break;
728
- case 'year':
729
- show = this.isWithinDays(rowDate, 365);
730
- break;
731
- }
732
- }
733
-
734
- row.style.display = show ? '' : 'none';
735
- });
732
+ this.tbody.replaceChildren(...shown);
736
733
  } else {
737
- // Card view filtering
738
- const cards = this.cardsContainer.querySelectorAll('.sitemap-card');
739
- cards.forEach(card => {
740
- let show = true;
741
-
742
- // Skip if already hidden by search
743
- if (card.style.display === 'none') return;
744
-
745
- // Collection filter
746
- if (collectionFilter && card.dataset.collection !== collectionFilter) {
747
- show = false;
748
- }
749
-
750
- // Date filter
751
- if (dateFilter && show) {
752
- const cardDate = new Date(card.dataset.date);
753
- const now = new Date();
754
-
755
- switch (dateFilter) {
756
- case 'today':
757
- show = this.isSameDay(cardDate, now);
758
- break;
759
- case 'week':
760
- show = this.isWithinDays(cardDate, 7);
761
- break;
762
- case 'month':
763
- show = this.isWithinDays(cardDate, 30);
764
- break;
765
- case 'year':
766
- show = this.isWithinDays(cardDate, 365);
767
- break;
768
- }
769
- }
770
-
771
- card.style.display = show ? '' : 'none';
772
- });
734
+ this.cardsContainer.replaceChildren(...shown.map(row => this.cardFor(row)));
735
+ }
736
+ if (this.showMoreWrap) {
737
+ const remaining = this.matches.length - shown.length;
738
+ this.showMoreWrap.classList.toggle('d-none', remaining <= 0);
739
+ if (this.showMoreBtn && remaining > 0) {
740
+ this.showMoreBtn.textContent = `Show ${Math.min(remaining, SitemapManager.PAGE_SIZE)} more (${remaining} remaining)`;
741
+ }
773
742
  }
774
-
775
- this.updateCounts();
743
+ this.updateCounts(shown.length);
776
744
  }
777
-
745
+
778
746
  sortTable(header) {
779
747
  const headerIndex = Array.from(header.parentNode.children).indexOf(header);
780
748
  const type = header.getAttribute('data-type');
781
749
  const currentOrder = header.getAttribute('data-order');
782
750
  const newOrder = currentOrder === 'asc' ? 'desc' : 'asc';
783
-
751
+
784
752
  // Clear all sort icons
785
753
  this.table.querySelectorAll('.sort-icon').forEach(icon => {
786
754
  icon.className = 'sort-icon bi bi-arrow-down-up ms-1';
787
755
  icon.classList.remove('active');
788
756
  });
789
-
757
+
790
758
  // Update current header
791
759
  header.setAttribute('data-order', newOrder);
792
760
  const icon = header.querySelector('.sort-icon');
793
761
  icon.className = `sort-icon bi bi-arrow-${newOrder === 'asc' ? 'up' : 'down'} ms-1 active`;
794
-
795
- // Sort rows
796
- const sortedRows = this.rows.sort((a, b) => {
762
+
763
+ // Sort every row (not just the rendered page), then re-render
764
+ this.rows.sort((a, b) => {
797
765
  let aValue = a.children[headerIndex].textContent.trim();
798
766
  let bValue = b.children[headerIndex].textContent.trim();
799
-
767
+
800
768
  if (type === 'date') {
801
769
  aValue = aValue === "Unknown" ? new Date(0) : new Date(aValue);
802
770
  bValue = bValue === "Unknown" ? new Date(0) : new Date(bValue);
803
771
  return newOrder === 'asc' ? aValue - bValue : bValue - aValue;
804
772
  }
805
-
806
- return newOrder === 'asc' ?
807
- aValue.localeCompare(bValue) :
773
+
774
+ return newOrder === 'asc' ?
775
+ aValue.localeCompare(bValue) :
808
776
  bValue.localeCompare(aValue);
809
777
  });
810
-
811
- // Reorder DOM
812
- sortedRows.forEach(row => this.tbody.appendChild(row));
778
+
779
+ const keep = this.limit;
780
+ this.applyFilters();
781
+ this.limit = keep;
782
+ this.render();
813
783
  }
814
-
815
- updateCounts() {
816
- let visibleCount = 0;
817
-
818
- if (this.currentView === 'table') {
819
- visibleCount = this.rows.filter(row => row.style.display !== 'none').length;
820
- } else {
821
- const cards = this.cardsContainer.querySelectorAll('.sitemap-card');
822
- visibleCount = Array.from(cards).filter(card => card.style.display !== 'none').length;
823
- }
824
-
825
- this.visibleCount.textContent = visibleCount;
826
- this.totalCount.textContent = this.rows.length;
827
-
784
+
785
+ updateCounts(shownCount) {
786
+ this.visibleCount.textContent = shownCount;
787
+ this.totalCount.textContent = this.matches.length;
788
+
828
789
  // Update page statistics
829
790
  document.getElementById('totalPages').textContent = this.rows.length;
830
791
  document.getElementById('totalCollections').textContent = this.collections.size;
831
792
  }
832
-
793
+
833
794
  updateStatistics() {
834
795
  const statsContainer = document.getElementById('collectionStats');
835
796
  const collectionCounts = {};
836
-
797
+
837
798
  this.rows.forEach(row => {
838
799
  const collection = row.dataset.collection;
839
800
  collectionCounts[collection] = (collectionCounts[collection] || 0) + 1;
840
801
  });
841
-
802
+
842
803
  const statsHtml = Object.entries(collectionCounts)
843
804
  .sort(([,a], [,b]) => b - a)
844
805
  .map(([collection, count]) => `
@@ -852,10 +813,10 @@ class SitemapManager {
852
813
  </div>
853
814
  </div>
854
815
  `).join('');
855
-
816
+
856
817
  statsContainer.innerHTML = statsHtml;
857
818
  }
858
-
819
+
859
820
  toggleStatistics() {
860
821
  this.statsSection.classList.toggle('d-none');
861
822
  const isVisible = !this.statsSection.classList.contains('d-none');
@@ -864,36 +825,29 @@ class SitemapManager {
864
825
  ${isVisible ? 'Hide' : 'Stats'}
865
826
  `;
866
827
  }
867
-
828
+
868
829
  resetFilters() {
869
830
  this.searchInput.value = '';
870
831
  this.collectionFilter.value = '';
871
832
  this.dateFilter.value = '';
872
-
873
- // Reset table rows
874
- this.rows.forEach(row => row.style.display = '');
875
-
876
- // Reset cards
877
- const cards = this.cardsContainer.querySelectorAll('.sitemap-card');
878
- cards.forEach(card => card.style.display = '');
879
-
880
- this.updateCounts();
881
-
833
+
834
+ this.applyFilters();
835
+
882
836
  // Clear URL parameters
883
837
  const url = new URL(window.location);
884
838
  url.searchParams.delete('q');
885
839
  window.history.replaceState({}, document.title, url);
886
840
  }
887
-
841
+
888
842
  copyUrl(event) {
889
- const url = event.currentTarget.dataset.url;
843
+ const btn = event.currentTarget;
844
+ const url = btn.dataset.url;
890
845
  navigator.clipboard.writeText(url).then(() => {
891
- const btn = event.currentTarget;
892
846
  const originalIcon = btn.innerHTML;
893
847
  btn.innerHTML = '<i class="bi bi-check"></i>';
894
848
  btn.classList.add('btn-success');
895
849
  btn.classList.remove('btn-outline-secondary');
896
-
850
+
897
851
  setTimeout(() => {
898
852
  btn.innerHTML = originalIcon;
899
853
  btn.classList.remove('btn-success');
@@ -901,36 +855,39 @@ class SitemapManager {
901
855
  }, 1500);
902
856
  });
903
857
  }
904
-
858
+
859
+ // Returns true when a ?q= query was applied (which renders the view).
905
860
  handleUrlParams() {
906
861
  const urlParams = new URLSearchParams(window.location.search);
907
862
  const searchQuery = urlParams.get('q');
908
-
863
+
909
864
  if (searchQuery) {
910
865
  this.searchInput.value = searchQuery;
911
866
  this.performSearch();
867
+ return true;
912
868
  }
869
+ return false;
913
870
  }
914
-
871
+
915
872
  handleKeyboardShortcuts(event) {
916
873
  // Ctrl/Cmd + K to focus search
917
874
  if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
918
875
  event.preventDefault();
919
876
  this.searchInput.focus();
920
877
  }
921
-
878
+
922
879
  // Escape to clear search
923
880
  if (event.key === 'Escape' && document.activeElement === this.searchInput) {
924
881
  this.searchInput.value = '';
925
882
  this.performSearch();
926
883
  }
927
884
  }
928
-
885
+
929
886
  // Utility functions
930
887
  isSameDay(date1, date2) {
931
888
  return date1.toDateString() === date2.toDateString();
932
889
  }
933
-
890
+
934
891
  isWithinDays(date, days) {
935
892
  const now = new Date();
936
893
  const diffTime = Math.abs(now - date);