Created
April 3, 2026 12:28
-
-
Save tijnjh/fa4a86a70d4f4960e149217f026ad067 to your computer and use it in GitHub Desktop.
DAS – Group Module Sections
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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