Skip to content

Instantly share code, notes, and snippets.

@tijnjh
Created April 3, 2026 12:28
Show Gist options
  • Select an option

  • Save tijnjh/fa4a86a70d4f4960e149217f026ad067 to your computer and use it in GitHub Desktop.

Select an option

Save tijnjh/fa4a86a70d4f4960e149217f026ad067 to your computer and use it in GitHub Desktop.
DAS – Group Module Sections
// ==UserScript==
// @name Group Module Sections
// @namespace fletcher-das
// @version 1.3
// @description Collapses Swagger UI tag sections into logical module groups (Modules / Foo /* → one collapsible)
// @match https://*.fletcher.build/docs
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const GROUP_DEPTH = 2;
const POLL_INTERVAL_MS = 400;
const MAX_POLLS = 60;
// Inject shared styles once
function injectStyles() {
if (document.getElementById('das-group-styles')) return;
const style = document.createElement('style');
style.id = 'das-group-styles';
style.textContent = `
.das-group-wrapper {
border: 1px solid var(--das-border, #49cc90);
border-radius: 6px;
margin-bottom: 16px;
overflow: hidden;
}
.das-group-header {
padding: 9px 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
background: var(--das-header-bg);
border-bottom: 1px solid var(--das-border);
}
.das-group-header:hover {
background: var(--das-header-hover-bg);
}
.das-group-title {
font-family: sans-serif;
font-size: 14px;
font-weight: 500;
color: var(--das-title-color);
}
.das-group-chevron {
font-size: 13px;
color: var(--das-accent);
display: inline-block;
margin-right: 8px;
transition: transform 0.15s;
line-height: 1;
}
.das-group-badge {
font-family: sans-serif;
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: 10px;
margin-left: 6px;
background: var(--das-badge-bg);
color: var(--das-accent);
border: 1px solid var(--das-border);
}
.das-group-body {
padding: 4px 0 0;
}
/* Light mode */
@media (prefers-color-scheme: light) {
:root {
--das-header-bg: #f0faf5;
--das-header-hover-bg: #e0f5ec;
--das-border: #49cc90;
--das-title-color: #1a4731;
--das-accent: #1a7a4a;
--das-badge-bg: #e0f5ec;
}
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--das-header-bg: #1a2e20;
--das-header-hover-bg: #21392a;
--das-border: #3b6d11;
--das-title-color: #c0dd97;
--das-accent: #97c459;
--das-badge-bg: #0c1a08;
}
}
`;
document.head.appendChild(style);
}
function getGroupKey(tagName) {
return tagName.split(' / ').slice(0, GROUP_DEPTH).join(' / ');
}
function countEndpoints(section) {
return section.querySelectorAll('.opblock').length;
}
function buildGroupWrapper(key, items) {
const totalEndpoints = items.reduce((sum, { section }) => sum + countEndpoints(section), 0);
const wrapper = document.createElement('div');
wrapper.className = 'das-group-wrapper';
// ── Header ──
const header = document.createElement('div');
header.className = 'das-group-header';
const left = document.createElement('div');
left.style.cssText = 'display:flex;align-items:center;';
const chevron = document.createElement('span');
chevron.className = 'das-group-chevron';
chevron.textContent = '▶';
chevron.style.transform = 'rotate(0deg)'; // starts collapsed → pointing right
const titleEl = document.createElement('span');
titleEl.className = 'das-group-title';
titleEl.textContent = key;
left.append(chevron, titleEl);
const right = document.createElement('div');
right.style.cssText = 'display:flex;align-items:center;';
const groupBadge = document.createElement('span');
groupBadge.className = 'das-group-badge';
groupBadge.textContent = `${items.length} groups`;
const epBadge = document.createElement('span');
epBadge.className = 'das-group-badge';
epBadge.textContent = `${totalEndpoints} endpoints`;
right.append(groupBadge, epBadge);
header.append(left, right);
// ── Body ── starts hidden (collapsed by default)
const body = document.createElement('div');
body.className = 'das-group-body';
body.style.display = 'none';
items.forEach(({ section }) => body.appendChild(section));
wrapper.append(header, body);
// ── Toggle ──
let collapsed = true;
header.addEventListener('click', () => {
collapsed = !collapsed;
body.style.display = collapsed ? 'none' : '';
chevron.style.transform = collapsed ? 'rotate(0deg)' : 'rotate(90deg)';
});
return wrapper;
}
function groupSections() {
const sections = [...document.querySelectorAll('.opblock-tag-section')];
if (!sections.length) return false;
if (document.querySelector('.das-group-wrapper')) return true;
const orderedKeys = [];
const groupMap = new Map();
sections.forEach((section) => {
const tagEl = section.querySelector('h3.opblock-tag');
if (!tagEl) return;
const tagName = tagEl.dataset.tag || tagEl.textContent.trim();
const key = getGroupKey(tagName);
if (!groupMap.has(key)) {
orderedKeys.push(key);
groupMap.set(key, []);
}
groupMap.get(key).push({ section, tagName });
});
orderedKeys.forEach((key) => {
const items = groupMap.get(key);
if (items.length < 2) return;
const firstSection = items[0].section;
const parent = firstSection.parentElement;
if (!parent) return;
// Sentinel anchors our insertion point before any sections are reparented
const sentinel = document.createComment(`das-group:${key}`);
parent.insertBefore(sentinel, firstSection);
const wrapper = buildGroupWrapper(key, items);
parent.replaceChild(wrapper, sentinel);
});
return true;
}
injectStyles();
let polls = 0;
const timer = setInterval(() => {
const ready = document.querySelectorAll('.opblock-tag-section').length > 3;
const done = ready && groupSections();
if (done || ++polls >= MAX_POLLS) clearInterval(timer);
}, POLL_INTERVAL_MS);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment