pub const TEMPLATE_HTML: &str = "<!doctype html>\n<html lang=\"en\" data-theme=\"{{theme}}\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title></title>\n <link rel=\"icon\" href=\"{{favicon}}\" />\n <link\n rel=\"stylesheet\"\n href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css\"\n />\n <link\n id=\"hljs-dark\"\n rel=\"stylesheet\"\n href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css\"\n />\n <link\n id=\"hljs-light\"\n rel=\"stylesheet\"\n href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-light.min.css\"\n disabled\n />\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/php.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js\"></script>\n <link\n rel=\"stylesheet\"\n href=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/codemirror.min.css\"\n />\n <link\n rel=\"stylesheet\"\n href=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/theme/atom-one-dark.min.css\"\n />\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/codemirror.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/mode/javascript/javascript.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/mode/xml/xml.min.js\"></script>\n <style>\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n {{light}}\n\n {{dark}}\n\n :root,\n [data-theme=\"dark\"] {\n /* nav active state */\n --nav-active-border: var(--accent);\n --nav-active-bg: var(--accent-bg);\n\n /* method badge border colours map to their method var */\n --m-get: var(--green);\n --m-post: var(--blue);\n --m-put: var(--orange);\n --m-patch: var(--purple);\n --m-delete: var(--red);\n --m-head: var(--cyan);\n }\n\n /* \u{2500}\u{2500} Element resets that depend on theme \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n body {\n background: var(--bg);\n color: var(--t1);\n }\n\n ::-webkit-scrollbar-track {\n background: var(--sb-t);\n }\n ::-webkit-scrollbar-thumb {\n background: var(--sb-th);\n }\n\n /* \u{2500}\u{2500} Sidebar \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .sidebar {\n background: var(--bg-sub);\n border-right-color: var(--bd);\n }\n .topbar {\n background: var(--bg-sub);\n border-bottom-color: var(--bd);\n }\n\n .sb-title {\n color: var(--t1);\n }\n .sb-ver {\n color: var(--t3);\n border-color: var(--bd);\n }\n .sb-oa {\n color: var(--t4);\n }\n\n .sb-srch input {\n background: var(--bg-card);\n border-color: var(--bd);\n color: var(--t1);\n }\n .sb-srch input::placeholder {\n color: var(--t4);\n }\n .sb-srch input:focus {\n border-color: var(--accent);\n }\n .sb-srch i {\n color: var(--t4);\n }\n\n .sb-grp-hd {\n color: var(--t3);\n }\n .sb-grp-hd .ln {\n background: var(--bd);\n }\n\n .ep-btn:hover {\n background: color-mix(in srgb, var(--t1) 4%, transparent);\n }\n .ep-btn.on {\n border-left-color: var(--accent);\n background: var(--accent-bg);\n }\n .ep-m {\n }\n .ep-p {\n color: var(--t2);\n }\n\n .sb-foot {\n color: var(--t3);\n border-top-color: var(--bd);\n }\n .sb-foot i {\n color: var(--green);\n }\n\n /* \u{2500}\u{2500} Theme toggle buttons \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .theme-btn {\n background: transparent;\n color: var(--t3);\n }\n .theme-btn:hover {\n color: var(--t1);\n }\n .theme-btn.on {\n color: var(--accent);\n background: var(--accent-bg);\n }\n\n /* \u{2500}\u{2500} Topbar \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .server-pill {\n background: var(--bg-card);\n border-color: var(--bd);\n color: var(--t2);\n }\n .server-url {\n color: var(--t2);\n }\n .auth-badge {\n color: var(--orange);\n border-color: color-mix(\n in srgb,\n var(--orange) 40%,\n transparent\n );\n }\n .import-btn {\n background: var(--bg-card);\n border-color: var(--bd);\n color: var(--t2);\n }\n .import-btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n }\n\n /* \u{2500}\u{2500} Endpoint header \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .ep-summary {\n color: var(--t1);\n }\n .ep-opid {\n color: var(--t3);\n }\n .ep-path-pill {\n color: var(--t1);\n }\n .auth-pill {\n color: var(--orange);\n border-color: var(--orange);\n }\n\n .mbg-get {\n background: color-mix(in srgb, var(--green) 8%, transparent);\n }\n .mbg-post {\n background: color-mix(in srgb, var(--blue) 8%, transparent);\n }\n .mbg-put {\n background: color-mix(in srgb, var(--orange) 8%, transparent);\n }\n .mbg-patch {\n background: color-mix(in srgb, var(--purple) 8%, transparent);\n }\n .mbg-delete {\n background: color-mix(in srgb, var(--red) 8%, transparent);\n }\n\n .ep-hdr {\n border-bottom-color: var(--bd);\n }\n .ep-body {\n }\n\n /* \u{2500}\u{2500} Section label \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .sec-lbl {\n color: var(--t3);\n }\n\n /* \u{2500}\u{2500} Params / body tables \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .params-tbl {\n border-color: var(--bd);\n }\n .p-row {\n border-bottom-color: var(--bd-sub);\n }\n .p-name {\n color: var(--t1);\n }\n .p-in {\n color: var(--t3);\n border-color: var(--bd);\n }\n .p-type {\n color: var(--cyan);\n }\n .p-desc {\n color: var(--t2);\n }\n .p-req {\n color: var(--red);\n }\n .enum-chip {\n background: var(--bg-code);\n border-color: var(--bd);\n color: var(--t2);\n }\n\n .rb-req {\n color: var(--red);\n border-color: var(--red);\n }\n .mp-note {\n border-color: var(--bd);\n color: var(--t2);\n }\n .mp-note i {\n color: var(--orange);\n }\n\n /* \u{2500}\u{2500} Code blocks \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .code-wrap {\n border-color: var(--bd);\n }\n .code-hd {\n background: var(--bg-code);\n border-bottom-color: var(--bd);\n }\n .code-hd-lbl {\n color: var(--t3);\n }\n .code-wrap pre {\n background: var(--bg-code) !important;\n }\n\n /* \u{2500}\u{2500} Language tabs \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .lang-tabs {\n background: var(--bg-code);\n border-bottom-color: var(--bd);\n }\n .lang-tab {\n color: var(--t3);\n border-bottom-color: transparent;\n }\n .lang-tab:hover {\n color: var(--t1);\n }\n .lang-tab.on {\n color: var(--accent);\n border-bottom-color: var(--accent);\n }\n\n /* \u{2500}\u{2500} Response tabs \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .resp-tab {\n border-color: var(--bd);\n color: var(--t3);\n }\n .resp-tab:hover {\n border-color: var(--t3);\n color: var(--t1);\n }\n .resp-tab.on {\n background: var(--bg-card);\n color: var(--t1);\n border-color: var(--t2);\n }\n .resp-box {\n border-color: var(--bd);\n }\n\n .s2 {\n color: var(--s2xx);\n }\n .s3 {\n color: var(--s3xx);\n }\n .s4 {\n color: var(--s4xx);\n }\n .s5 {\n color: var(--s5xx);\n }\n\n /* \u{2500}\u{2500} Copy button \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .copy-btn {\n background: var(--bg-sub);\n border-color: var(--bd);\n color: var(--t3);\n }\n .copy-btn:hover {\n color: var(--t1);\n border-color: var(--t3);\n }\n .copy-btn.ok {\n color: var(--green);\n border-color: var(--green);\n }\n\n /* \u{2500}\u{2500} Method badge border colours \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .mb-get {\n border-color: var(--green);\n }\n .mb-post {\n border-color: var(--blue);\n }\n .mb-put {\n border-color: var(--orange);\n }\n .mb-patch {\n border-color: var(--purple);\n }\n .mb-delete {\n border-color: var(--red);\n }\n\n /* \u{2500}\u{2500} Method text colours \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .m-get {\n color: var(--green);\n }\n .m-post {\n color: var(--blue);\n }\n .m-put {\n color: var(--orange);\n }\n .m-patch {\n color: var(--purple);\n }\n .m-delete {\n color: var(--red);\n }\n .m-head {\n color: var(--cyan);\n }\n\n /* \u{2500}\u{2500} Sidebar logo \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .sb-logo {\n background: var(--accent);\n }\n\n /* \u{2500}\u{2500} Empty state \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .empty {\n color: var(--t3);\n }\n\n /* \u{2500}\u{2500} Import modal \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .modal-ov {\n background: rgba(0, 0, 0, 0.55);\n }\n .modal-box {\n background: var(--bg-card);\n border-color: var(--bd);\n box-shadow: var(--shadow-md);\n }\n .modal-hd {\n border-bottom-color: var(--bd);\n }\n .modal-ttl {\n color: var(--t1);\n }\n .modal-sub {\n color: var(--t3);\n }\n .modal-cls {\n color: var(--t3);\n }\n .modal-cls:hover {\n color: var(--t1);\n }\n .modal-ta {\n background: var(--bg-code);\n border-color: var(--bd);\n color: var(--t1);\n }\n .modal-ta:focus {\n border-color: var(--accent);\n }\n .modal-err {\n color: var(--red);\n }\n\n /* \u{2500}\u{2500} Buttons \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .btn-p {\n background: var(--accent);\n color: #fff;\n }\n .btn-p:hover {\n background: var(--accent-h);\n opacity: 1;\n }\n .btn-s {\n border-color: var(--bd);\n color: var(--t2);\n }\n .btn-s:hover {\n border-color: var(--t2);\n color: var(--t1);\n }\n html,\n body,\n #root {\n height: 100%;\n }\n body {\n font-family:\n -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n \"Helvetica Neue\", Arial, sans-serif;\n font-size: 14px;\n line-height: 1.5;\n background: var(--bg);\n color: var(--t1);\n transition:\n background 0.2s,\n color 0.2s;\n }\n ::-webkit-scrollbar {\n width: 5px;\n height: 5px;\n }\n ::-webkit-scrollbar-track {\n background: var(--sb-t);\n }\n ::-webkit-scrollbar-thumb {\n background: var(--sb-th);\n border-radius: 3px;\n }\n button {\n font-family: inherit;\n cursor: pointer;\n }\n input,\n textarea {\n font-family: inherit;\n }\n a {\n text-decoration: none;\n }\n .app {\n display: flex;\n height: 100vh;\n overflow: hidden;\n }\n .sidebar {\n width: 265px;\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: var(--bg-sub);\n border-right: 1px solid var(--bd);\n overflow: hidden;\n }\n .main {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n min-width: 0;\n }\n .topbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0 24px;\n height: 48px;\n flex-shrink: 0;\n background: var(--bg-sub);\n border-bottom: 1px solid var(--bd);\n gap: 12px;\n }\n .detail {\n flex: 1;\n overflow-y: auto;\n }\n .sb-hdr {\n padding: 14px 16px;\n border-bottom: 1px solid var(--bd);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n .sb-brand {\n display: flex;\n align-items: center;\n gap: 10px;\n flex: 1;\n }\n .sb-logo {\n width: auto;\n height: 32px;\n border-radius: 8px;\n overflow: hidden;\n flex-shrink: 0;\n }\n .sb-logo.sb-logo-img {\n background: transparent;\n }\n .sb-logo img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n background: transparent;\n }\n .sb-title {\n font-size: 14px;\n font-weight: 700;\n color: var(--t1);\n }\n .sb-meta {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-top: 3px;\n }\n .sb-ver {\n font-size: 10px;\n font-family: monospace;\n color: var(--t3);\n border: 1px solid var(--bd);\n border-radius: 3px;\n padding: 1px 5px;\n }\n .sb-oa {\n font-size: 10px;\n color: var(--t4);\n }\n .sb-themes {\n display: flex;\n gap: 4px;\n padding: 8px 12px;\n flex-shrink: 0;\n margin-left: auto;\n }\n .theme-btn {\n width: 32px;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 5px;\n font-size: 14px;\n background: transparent;\n border: 1px solid transparent;\n color: var(--t3);\n transition: all 0.15s;\n }\n .theme-btn:hover {\n color: var(--t1);\n }\n .theme-btn.on {\n color: var(--accent);\n background: color-mix(in srgb, var(--accent) 8%, transparent);\n }\n .theme-btn:hover {\n color: var(--t1);\n }\n .theme-btn.on {\n color: var(--accent);\n border-color: var(--accent);\n background: color-mix(in srgb, var(--accent) 8%, transparent);\n }\n .sb-srch {\n padding: 8px 12px;\n border-bottom: 1px solid var(--bd);\n flex-shrink: 0;\n position: relative;\n }\n .sb-srch i {\n position: absolute;\n left: 23px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--t4);\n font-size: 11px;\n pointer-events: none;\n }\n .sb-srch input {\n width: 100%;\n padding: 7px 10px 7px 30px;\n border-radius: 6px;\n border: 1px solid var(--bd);\n background: var(--bg-card);\n color: var(--t1);\n font-size: 12px;\n transition: border-color 0.15s;\n }\n .sb-srch input::placeholder {\n color: var(--t4);\n }\n .sb-srch input:focus {\n outline: none;\n border-color: var(--accent);\n }\n .sb-nav {\n flex: 1;\n overflow-y: auto;\n padding: 4px 0 8px;\n }\n .sb-grp-hd {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px 3px;\n font-size: 11px;\n font-weight: 600;\n color: var(--t3);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n cursor: pointer;\n user-select: none;\n transition: all 0.15s;\n }\n .sb-grp-hd:hover {\n color: var(--t1);\n }\n .ep-btn {\n width: 100%;\n text-align: left;\n padding: 7px 16px;\n display: flex;\n align-items: center;\n gap: 10px;\n background: none;\n border: none;\n border-left: 2px solid transparent;\n transition:\n background 0.12s,\n border-color 0.12s;\n }\n\n .ep-btn:hover {\n background: color-mix(in srgb, var(--t1) 4%, transparent);\n }\n .ep-btn.on {\n border-left-color: var(--accent);\n background: color-mix(in srgb, var(--accent) 6%, transparent);\n }\n .ep-m {\n font-family: monospace;\n font-size: 10px;\n font-weight: 700;\n width: 46px;\n text-align: right;\n flex-shrink: 0;\n }\n .ep-p {\n font-size: 12px;\n color: var(--t2);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-family: monospace;\n }\n .sb-foot {\n padding: 10px 16px;\n border-top: 1px solid var(--bd);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 11px;\n color: var(--t3);\n }\n .m-get {\n color: var(--green);\n }\n .m-post {\n color: var(--accent);\n }\n .m-put {\n color: var(--orange);\n }\n .m-patch {\n color: var(--purple);\n }\n .m-delete {\n color: var(--red);\n }\n .m-head {\n color: var(--cyan);\n }\n .mb-get {\n border-color: var(--green);\n }\n .mb-post {\n border-color: var(--accent);\n }\n .mb-put {\n border-color: var(--orange);\n }\n .mb-patch {\n border-color: var(--purple);\n }\n .mb-delete {\n border-color: var(--red);\n }\n .mbg-get {\n background: color-mix(in srgb, var(--green) 8%, transparent);\n }\n .mbg-post {\n background: color-mix(in srgb, var(--accent) 8%, transparent);\n }\n .mbg-put {\n background: color-mix(in srgb, var(--orange) 8%, transparent);\n }\n .mbg-patch {\n background: color-mix(in srgb, var(--purple) 8%, transparent);\n }\n .mbg-delete {\n background: color-mix(in srgb, var(--red) 8%, transparent);\n }\n .method-badge {\n font-family: monospace;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n padding: 2px 8px;\n border-radius: 4px;\n border: 1px solid;\n }\n .server-pill {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n background: var(--bg-card);\n border: 1px solid var(--bd);\n border-radius: 5px;\n padding: 4px 10px;\n color: var(--t2);\n }\n .server-url {\n font-family: monospace;\n }\n .auth-badge {\n display: flex;\n align-items: center;\n gap: 5px;\n font-size: 11px;\n color: var(--orange);\n border: 1px solid\n color-mix(in srgb, var(--orange) 40%, transparent);\n border-radius: 5px;\n padding: 4px 10px;\n }\n .import-btn {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n background: var(--bg-card);\n border: 1px solid var(--bd);\n color: var(--t2);\n border-radius: 6px;\n padding: 6px 12px;\n transition: all 0.15s;\n }\n .import-btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n }\n .ep-hdr {\n padding: 24px 32px;\n border-bottom: 1px solid var(--bd);\n }\n .ep-hdr-row {\n display: flex;\n align-items: flex-start;\n gap: 16px;\n }\n .ep-icon {\n width: 40px;\n height: 40px;\n border-radius: 8px;\n border: 1px solid;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 15px;\n flex-shrink: 0;\n }\n .ep-summary {\n font-size: 20px;\n font-weight: 700;\n color: var(--t1);\n margin-top: 6px;\n }\n .ep-opid {\n font-family: monospace;\n font-size: 10px;\n color: var(--t3);\n margin-top: 4px;\n }\n .ep-path-pill {\n font-family: monospace;\n font-size: 13px;\n color: var(--t1);\n }\n .auth-pill {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 10px;\n color: var(--orange);\n border: 1px solid var(--orange);\n border-radius: 4px;\n padding: 2px 7px;\n }\n .ep-body {\n padding: 24px 32px;\n }\n .sec-lbl {\n font-size: 11px;\n font-weight: 600;\n color: var(--t3);\n text-transform: uppercase;\n letter-spacing: 0.08em;\n margin-bottom: 10px;\n }\n .params-tbl {\n border: 1px solid var(--bd);\n border-radius: 8px;\n overflow: hidden;\n margin-bottom: 20px;\n }\n .p-row {\n display: flex;\n align-items: flex-start;\n gap: 14px;\n padding: 11px 16px;\n font-size: 13px;\n border-bottom: 1px solid var(--bd-sub);\n }\n .p-row:last-child {\n border-bottom: none;\n }\n .p-name {\n width: 130px;\n flex-shrink: 0;\n font-family: monospace;\n font-weight: 600;\n font-size: 12px;\n }\n .p-meta {\n width: 90px;\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n gap: 3px;\n }\n .p-in {\n font-size: 10px;\n font-family: monospace;\n border: 1px solid var(--bd);\n border-radius: 3px;\n padding: 1px 5px;\n color: var(--t3);\n width: fit-content;\n }\n .p-type {\n font-size: 10px;\n color: var(--cyan);\n font-family: monospace;\n }\n .p-desc {\n flex: 1;\n color: var(--t2);\n line-height: 1.5;\n }\n .p-req {\n color: var(--red);\n font-size: 10px;\n margin-left: 2px;\n }\n .enum-chip {\n display: inline-block;\n font-family: monospace;\n font-size: 10px;\n background: var(--bg-code);\n border: 1px solid var(--bd);\n border-radius: 3px;\n padding: 1px 6px;\n color: var(--t2);\n margin: 2px 2px 0 0;\n }\n .code-wrap {\n border: 1px solid var(--bd);\n border-radius: 8px;\n overflow: hidden;\n margin-bottom: 16px;\n }\n .code-hd {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 7px 14px;\n background: var(--bg-code);\n border-bottom: 1px solid var(--bd);\n }\n .code-hd-lbl {\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--t3);\n }\n .code-wrap pre {\n padding: 16px;\n overflow-x: auto;\n background: var(--bg-code) !important;\n margin: 0;\n }\n .hljs {\n background: transparent !important;\n padding: 0 !important;\n font-size: 12px !important;\n line-height: 1.65 !important;\n font-family:\n ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,\n monospace !important;\n }\n .copy-btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-size: 11px;\n background: var(--bg-sub);\n border: 1px solid var(--bd);\n color: var(--t3);\n border-radius: 4px;\n padding: 3px 9px;\n transition: all 0.15s;\n }\n .copy-btn:hover {\n color: var(--t1);\n border-color: var(--t3);\n }\n .copy-btn.ok {\n color: var(--green);\n border-color: var(--green);\n }\n .lang-tabs {\n display: flex;\n overflow-x: auto;\n background: var(--bg-code);\n border-bottom: 1px solid var(--bd);\n }\n .lang-tab {\n padding: 8px 14px;\n font-size: 12px;\n white-space: nowrap;\n background: none;\n border: none;\n border-bottom: 2px solid transparent;\n color: var(--t3);\n transition: all 0.15s;\n display: flex;\n align-items: center;\n gap: 6px;\n }\n .lang-tab:hover {\n color: var(--t1);\n }\n .lang-tab.on {\n color: var(--accent);\n border-bottom-color: var(--accent);\n }\n .resp-tabs {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-bottom: 12px;\n }\n .resp-tab {\n font-family: monospace;\n font-size: 12px;\n border: 1px solid var(--bd);\n border-radius: 5px;\n padding: 4px 12px;\n background: transparent;\n color: var(--t3);\n transition: all 0.15s;\n }\n .resp-tab:hover {\n border-color: var(--t3);\n color: var(--t1);\n }\n .resp-tab.on {\n background: var(--bg-card);\n color: var(--t1);\n border-color: var(--t2);\n }\n .resp-box {\n border: 1px solid var(--bd);\n border-radius: 8px;\n padding: 14px 16px;\n }\n .s2 {\n color: var(--green);\n }\n .s3 {\n color: var(--cyan);\n }\n .s4 {\n color: var(--orange);\n }\n .s5 {\n color: var(--red);\n }\n .rb-hd {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 10px;\n }\n .rb-req {\n font-size: 10px;\n color: var(--red);\n border: 1px solid var(--red);\n border-radius: 3px;\n padding: 1px 7px;\n }\n .mp-note {\n padding: 10px 14px;\n border: 1px solid var(--bd);\n border-radius: 6px;\n font-size: 12px;\n color: var(--t2);\n margin-bottom: 12px;\n }\n .empty {\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n gap: 12px;\n color: var(--t3);\n }\n .empty i {\n font-size: 36px;\n opacity: 0.2;\n }\n .empty p {\n font-size: 13px;\n }\n .modal-ov {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.55);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 200;\n padding: 16px;\n }\n .modal-box {\n background: var(--bg-card);\n border: 1px solid var(--bd);\n border-radius: 12px;\n width: 100%;\n max-width: 620px;\n box-shadow: 0 24px 64px rgba(0, 0, 0, 0.3);\n }\n .modal-hd {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 18px 24px;\n border-bottom: 1px solid var(--bd);\n }\n .modal-ttl {\n font-size: 16px;\n font-weight: 700;\n color: var(--t1);\n }\n .modal-sub {\n font-size: 11px;\n color: var(--t3);\n margin-top: 2px;\n }\n .modal-cls {\n background: none;\n border: none;\n color: var(--t3);\n font-size: 18px;\n padding: 4px;\n border-radius: 4px;\n transition: color 0.15s;\n }\n .modal-cls:hover {\n color: var(--t1);\n }\n .modal-bdy {\n padding: 20px 24px;\n }\n .modal-ta {\n width: 100%;\n height: 240px;\n resize: none;\n padding: 12px;\n background: var(--bg-code);\n border: 1px solid var(--bd);\n border-radius: 6px;\n color: var(--t1);\n font-family: monospace;\n font-size: 12px;\n }\n .modal-ta:focus {\n outline: none;\n border-color: var(--accent);\n }\n .modal-err {\n font-size: 12px;\n color: var(--red);\n margin-top: 8px;\n display: flex;\n align-items: center;\n gap: 6px;\n }\n .modal-btns {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 10px;\n margin-top: 16px;\n }\n .btn-p {\n background: var(--accent);\n color: #fff;\n border: none;\n border-radius: 6px;\n padding: 9px 0;\n font-size: 13px;\n font-weight: 600;\n transition: opacity 0.15s;\n width: 100%;\n }\n .btn-p:hover {\n opacity: 0.88;\n }\n .btn-s {\n background: transparent;\n border: 1px solid var(--bd);\n color: var(--t2);\n border-radius: 6px;\n padding: 9px 0;\n font-size: 13px;\n transition: all 0.15s;\n width: 100%;\n }\n .btn-s:hover {\n border-color: var(--t2);\n color: var(--t1);\n }\n @keyframes fadeUp {\n from {\n opacity: 0;\n transform: translateY(6px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n .fu {\n animation: fadeUp 0.25s ease forwards;\n }\n\n /* \u{2500}\u{2500} API Testing UI \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500} */\n .test-panel {\n background: var(--bg-card);\n border: 1px solid var(--bd);\n border-radius: 8px;\n padding: 16px;\n margin-bottom: 20px;\n }\n .test-panel-hd {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 12px;\n }\n .test-panel-ttl {\n font-size: 13px;\n font-weight: 700;\n color: var(--t1);\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .auth-input-grp {\n display: flex;\n gap: 8px;\n align-items: center;\n margin-bottom: 12px;\n }\n .auth-input {\n flex: 1;\n padding: 8px 12px;\n border-radius: 6px;\n border: 1px solid var(--bd);\n background: var(--bg-code);\n color: var(--t1);\n font-size: 12px;\n font-family: monospace;\n transition: border-color 0.15s;\n }\n .auth-input:focus {\n outline: none;\n border-color: var(--accent);\n }\n .auth-input::placeholder {\n color: var(--t3);\n }\n .btn-test {\n background: var(--accent);\n color: #fff;\n border: none;\n border-radius: 6px;\n padding: 8px 16px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.15s;\n display: flex;\n align-items: center;\n gap: 6px;\n }\n .btn-test:hover {\n background: var(--accent-h);\n transform: translateY(-1px);\n }\n .btn-test:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n transform: none;\n }\n .btn-test.loading {\n animation: pulse 1.5s ease-in-out infinite;\n }\n @keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.6;\n }\n }\n .param-input-row {\n display: flex;\n gap: 10px;\n align-items: center;\n margin-bottom: 10px;\n }\n .param-input-row:last-child {\n margin-bottom: 0;\n }\n .param-label {\n width: 120px;\n flex-shrink: 0;\n font-family: monospace;\n font-size: 12px;\n color: var(--t2);\n }\n .param-input {\n flex: 1;\n padding: 8px 12px;\n border-radius: 6px;\n border: 1px solid var(--bd);\n background: var(--bg-code);\n color: var(--t1);\n font-size: 12px;\n font-family: monospace;\n transition: border-color 0.15s;\n }\n .param-input:focus {\n outline: none;\n border-color: var(--accent);\n }\n .param-input::placeholder {\n color: var(--t3);\n }\n .param-type {\n width: 100px;\n flex-shrink: 0;\n font-size: 10px;\n color: var(--cyan);\n font-family: monospace;\n text-align: right;\n }\n .req-body-ta {\n width: 100%;\n min-height: 150px;\n padding: 12px;\n border-radius: 6px;\n border: 1px solid var(--bd);\n background: var(--bg-code);\n color: var(--t1);\n font-size: 12px;\n font-family:\n ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,\n monospace;\n resize: vertical;\n transition: border-color 0.15s;\n }\n .req-body-ta:focus {\n outline: none;\n border-color: var(--accent);\n }\n .code-editor-wrapper {\n border: 1px solid var(--bd);\n border-radius: 6px;\n overflow: hidden;\n }\n .code-editor-wrapper .CodeMirror {\n height: auto;\n min-height: 70px;\n font-family:\n ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,\n monospace;\n font-size: 12px;\n line-height: 1.5;\n color: var(--t1);\n background: var(--bg-code);\n }\n .code-editor-wrapper .CodeMirror-scroll {\n height: auto;\n overflow-y: hidden;\n overflow-x: auto;\n min-height: 70px;\n }\n .code-editor-wrapper .CodeMirror-sizer {\n min-height: 70px !important;\n }\n .code-editor-wrapper .CodeMirror-gutters {\n background: var(--bg-code);\n border-right: 1px solid var(--bd);\n min-height: 70px;\n }\n .code-editor-wrapper .CodeMirror-linenumber {\n color: var(--t3);\n }\n .code-editor-wrapper .error-gutter {\n width: 20px;\n }\n .code-editor-wrapper .CodeMirror-cursor {\n border-color: var(--accent);\n }\n .code-editor-wrapper .CodeMirror-selected {\n background: color-mix(\n in srgb,\n var(--accent) 20%,\n transparent\n ) !important;\n }\n .code-editor-wrapper .cm-property {\n color: #d19a66 !important;\n }\n .code-editor-wrapper .cm-attribute {\n color: #d19a66 !important;\n }\n .code-editor-wrapper .cm-string {\n color: #98c379;\n }\n .code-editor-wrapper .cm-number {\n color: #d19a66;\n }\n .code-editor-wrapper .cm-atom {\n color: #c678dd;\n }\n .code-editor-wrapper .cm-bool {\n color: #d19a66;\n }\n .code-editor-wrapper .cm-bracket {\n color: #abb2bf;\n }\n .code-editor-wrapper .cm-punctuation {\n color: #abb2bf;\n }\n .code-editor-wrapper .cm-json-error {\n background: rgba(224, 92, 75, 0.2);\n border-bottom: 2px wavy #e05c4b;\n }\n .json-error-hint {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: #e05c4b;\n background: rgba(224, 92, 75, 0.1);\n padding: 6px 10px;\n border-radius: 4px;\n margin-top: 8px;\n }\n .auth-header-input {\n margin-bottom: 12px;\n }\n .auth-header-label {\n font-size: 11px;\n font-weight: 600;\n color: var(--t3);\n text-transform: uppercase;\n letter-spacing: 0.08em;\n margin-bottom: 6px;\n display: flex;\n align-items: center;\n gap: 6px;\n }\n .auth-header-input input {\n width: 100%;\n padding: 8px 12px;\n border-radius: 6px;\n border: 1px solid var(--bd);\n background: var(--bg-code);\n color: var(--t1);\n font-size: 12px;\n font-family: monospace;\n transition: border-color 0.15s;\n }\n .auth-header-input input:focus {\n outline: none;\n border-color: var(--accent);\n }\n .auth-header-input input::placeholder {\n color: var(--t3);\n }\n .response-area {\n margin-top: 16px;\n }\n .response-hd {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 10px;\n }\n .response-ttl {\n font-size: 12px;\n font-weight: 700;\n color: var(--t1);\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .response-status {\n font-family: monospace;\n font-size: 11px;\n padding: 3px 8px;\n border-radius: 4px;\n border: 1px solid;\n }\n .response-status.success {\n color: var(--green);\n border-color: var(--green);\n background: color-mix(in srgb, var(--green) 8%, transparent);\n }\n .response-status.error {\n color: var(--red);\n border-color: var(--red);\n background: color-mix(in srgb, var(--red) 8%, transparent);\n }\n .response-status.info {\n color: var(--cyan);\n border-color: var(--cyan);\n background: color-mix(in srgb, var(--cyan) 8%, transparent);\n }\n .response-box {\n border: 1px solid var(--bd);\n border-radius: 6px;\n overflow: hidden;\n }\n .response-pre {\n margin: 0;\n padding: 14px;\n background: var(--bg-code);\n overflow-x: auto;\n max-height: 400px;\n overflow-y: auto;\n }\n .response-code {\n font-family:\n ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,\n monospace;\n font-size: 11px;\n line-height: 1.6;\n color: var(--t1);\n }\n .empty-response {\n padding: 24px;\n text-align: center;\n color: var(--t3);\n font-size: 12px;\n }\n .empty-response i {\n font-size: 24px;\n margin-bottom: 8px;\n opacity: 0.3;\n }\n .test-url {\n font-family: monospace;\n font-size: 11px;\n color: var(--t2);\n background: var(--bg-code);\n padding: 6px 10px;\n border-radius: 4px;\n margin-bottom: 12px;\n overflow-x: auto;\n white-space: nowrap;\n }\n .test-method {\n font-family: monospace;\n font-size: 10px;\n font-weight: 700;\n padding: 2px 6px;\n border-radius: 3px;\n border: 1px solid;\n margin-right: 6px;\n }\n .auth-status {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--t3);\n padding: 6px 10px;\n background: var(--bg-code);\n border-radius: 6px;\n }\n .auth-status.set {\n color: var(--green);\n background: color-mix(in srgb, var(--green) 8%, transparent);\n }\n .auth-status i {\n font-size: 10px;\n }\n .test-tabs {\n display: flex;\n gap: 4px;\n margin-bottom: 12px;\n border-bottom: 1px solid var(--bd);\n padding-bottom: 8px;\n }\n .test-tab {\n padding: 6px 12px;\n font-size: 11px;\n font-weight: 600;\n color: var(--t3);\n background: transparent;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.15s;\n }\n .test-tab:hover {\n color: var(--t1);\n background: var(--bg-code);\n }\n .test-tab.on {\n color: var(--accent);\n background: var(--accent-bg);\n }\n .test-tab-icon {\n margin-right: 4px;\n font-size: 10px;\n }\n </style>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\">\n import {\n createElement as h,\n useState,\n useEffect,\n useRef,\n useCallback,\n useMemo,\n Fragment,\n } from \"https://esm.sh/react@19\";\n import { createRoot } from \"https://esm.sh/react-dom@19/client\";\n\n // Injected OpenAPI spec - replaced by Rust template engine\n const INJECTED_SPEC = /* SPEC_JSON_PLACEHOLDER */ null;\n // Embedded sample data for \"Try Sample API\" button\n const SAMPLE_DATA = /* SAMPLE_DATA_PLACEHOLDER */ null;\n\n const METHOD_ORDER = [\n \"get\",\n \"post\",\n \"put\",\n \"patch\",\n \"delete\",\n \"head\",\n \"options\",\n ];\n const METHOD_ICON = {\n get: \"fa-download\",\n post: \"fa-plus\",\n put: \"fa-rotate-right\",\n patch: \"fa-pen\",\n delete: \"fa-trash\",\n head: \"fa-eye\",\n options: \"fa-gear\",\n };\n const LANGS = [\n { id: \"curl\", label: \"cURL\", fa: \"fa fa-terminal\", hl: \"bash\" },\n {\n id: \"javascript\",\n label: \"JavaScript\",\n fa: \"fab fa-js\",\n hl: \"javascript\",\n },\n {\n id: \"python\",\n label: \"Python\",\n fa: \"fab fa-python\",\n hl: \"python\",\n },\n { id: \"php\", label: \"PHP\", fa: \"fab fa-php\", hl: \"php\" },\n { id: \"go\", label: \"Go\", fa: \"fab fa-golang\", hl: \"go\" },\n { id: \"rust\", label: \"Rust\", fa: \"fab fa-rust\", hl: \"rust\" },\n ];\n\n function sCls(c) {\n const n = parseInt(c);\n return n < 300 ? \"s2\" : n < 400 ? \"s3\" : n < 500 ? \"s4\" : \"s5\";\n }\n\n function resolveRef(ref, spec) {\n if (!ref || !ref.startsWith(\"#/\")) return null;\n return (\n ref\n .slice(2)\n .split(\"/\")\n .reduce((o, k) => o?.[k], spec) ?? null\n );\n }\n\n function schemaEx(schema, spec, d = 0, fieldName = \"\") {\n if (!schema || d > 4) return null;\n if (schema.$ref) schema = resolveRef(schema.$ref, spec) || schema;\n if (schema.example !== undefined) return schema.example;\n if (schema.properties) {\n const o = {};\n for (const [k, v] of Object.entries(schema.properties)) {\n const r = v.$ref ? resolveRef(v.$ref, spec) || v : v;\n const t = Array.isArray(r.type) ? r.type[0] : r.type;\n o[k] =\n r.example ??\n schemaEx(r, spec, d + 1, k) ??\n (t === \"string\"\n ? r.format === \"email\"\n ? \"user@example.com\"\n : r.format === \"date-time\"\n ? new Date().toISOString()\n : r.format === \"uuid\"\n ? \"550e8400-e29b-41d4-a716-446655440000\"\n : (r.enum?.[0] ?? \"\")\n : t === \"integer\" || t === \"number\"\n ? (r.enum?.[0] ?? 0)\n : t === \"boolean\"\n ? false\n : null);\n }\n return o;\n }\n if (schema.enum) return schema.enum[0];\n if (schema.type === \"string\") {\n if (schema.format === \"email\") return \"user@example.com\";\n if (schema.format === \"date-time\") return new Date().toISOString();\n if (schema.format === \"uuid\")\n return \"550e8400-e29b-41d4-a716-446655440000\";\n if (schema.format === \"date\")\n return new Date().toISOString().split(\"T\")[0];\n const name = fieldName || schema.name || \"\";\n const lower = name.toLowerCase();\n if (lower.includes(\"email\")) return \"user@example.com\";\n if (lower.includes(\"phone\")) return \"+1234567890\";\n if (lower.includes(\"name\")) return \"Buddy\";\n if (lower.includes(\"password\")) return \"SecurePass123!\";\n if (lower.includes(\"token\")) return \"tok_\" + \"x\".repeat(24);\n if (lower.includes(\"id\") || lower.includes(\"_id\"))\n return \"usr_\" + Math.random().toString(36).slice(2, 10);\n if (lower.includes(\"code\")) return \"CODE123\";\n if (lower.includes(\"url\") || lower.includes(\"uri\"))\n return \"https://example.com\";\n if (lower.includes(\"currency\")) return \"USD\";\n if (lower.includes(\"status\")) return \"active\";\n if (lower.includes(\"title\")) return \"Sample Title\";\n if (lower.includes(\"description\") || lower.includes(\"narration\"))\n return \"Sample description\";\n if (lower.includes(\"reference\"))\n return \"ref_\" + Math.random().toString(36).slice(2, 10);\n if (lower.includes(\"account\")) return \"1234567890\";\n if (lower.includes(\"bank\")) return \"bank_code_123\";\n if (lower.includes(\"amount\")) return 100;\n if (lower.includes(\"platform\")) return \"ios\";\n if (lower.includes(\"device\")) return \"iPhone 15 Pro\";\n return \"\";\n }\n if (schema.type === \"integer\" || schema.type === \"number\") {\n const name = (fieldName || schema.name || \"\").toLowerCase();\n if (\n name.includes(\"amount\") ||\n name.includes(\"price\") ||\n name.includes(\"total\")\n )\n return 100;\n if (name.includes(\"age\")) return 30;\n if (name.includes(\"count\") || name.includes(\"limit\")) return 10;\n if (name.includes(\"page\")) return 1;\n if (name.includes(\"port\")) return 8080;\n return schema.enum?.[0] ?? 0;\n }\n if (schema.type === \"boolean\") return false;\n if (schema.type === \"array\")\n return [schemaEx(schema.items, spec, d + 1, fieldName)].filter(\n (x) => x != null,\n );\n return null;\n }\n\n function getBodyEx(op, spec) {\n const content = op.requestBody?.content;\n if (!content) return null;\n const ct =\n content[\"application/json\"] || content[Object.keys(content)[0]];\n if (!ct) return null;\n if (ct.example) return ct.example;\n if (ct.schema) {\n const s = ct.schema.$ref\n ? resolveRef(ct.schema.$ref, spec)\n : ct.schema;\n return schemaEx(s, spec);\n }\n return null;\n }\n\n function buildEps(spec) {\n const out = [];\n if (!spec?.paths) return out;\n for (const [rawPath, methods] of Object.entries(spec.paths)) {\n for (const [key, val] of Object.entries(methods)) {\n if (METHOD_ORDER.includes(key)) {\n out.push({\n path: rawPath,\n method: key,\n op: val,\n id: `${key}::${rawPath}`,\n });\n } else if (\n typeof val === \"object\" &&\n val !== null &&\n !Array.isArray(val)\n ) {\n const np = rawPath + key;\n for (const [m, op] of Object.entries(val)) {\n if (\n METHOD_ORDER.includes(m) &&\n !out.find((e) => e.id === `${m}::${np}`)\n )\n out.push({\n path: np,\n method: m,\n op,\n id: `${m}::${np}`,\n });\n }\n }\n }\n }\n return out;\n }\n\n function groupByTag(eps, tags) {\n const g = {};\n for (const ep of eps) {\n const tgs = ep.op.tags?.length ? ep.op.tags : [\"other\"];\n for (const t of tgs) {\n if (t) (g[t] = g[t] || []).push(ep);\n }\n }\n\n const o = {};\n const tagList = tags || [];\n // Handle both [{name: \'tag\'}] and [\'tag\'] formats\n for (const t of tagList) {\n const name = typeof t === \"string\" ? t : t?.name;\n if (name && g[name]) {\n o[name] = g[name];\n }\n }\n\n // Add tags that were not in the explicit tags list\n for (const t of Object.keys(g)) {\n if (!o[t]) {\n o[t] = g[t];\n }\n }\n return o;\n }\n\n function genCode(lang, ep, spec) {\n const base = spec.servers?.[0]?.url || \"\";\n const { path, method, op } = ep;\n const url = base + path;\n const body = getBodyEx(op, spec);\n const bodyStr = body ? JSON.stringify(body, null, 2) : null;\n const params = (op.parameters || []).filter((p) => p.in === \"query\");\n const qStr = params.length\n ? \"?\" +\n params\n .map((p) => `${p.name}=${p.example || \"<\" + p.name + \">\"}`)\n .join(\"&\")\n : \"\";\n const isMP = !!op.requestBody?.content?.[\"multipart/form-data\"];\n const hasBody = !!bodyStr && !isMP;\n const M = method.toUpperCase();\n\n if (lang === \"curl\") {\n const lines = [\n `curl -X ${M} \'${url}${qStr}\'`,\n ` -H \'X-API-Key: YOUR_API_KEY\'`,\n ];\n if (hasBody) {\n lines.push(` -H \'Content-Type: application/json\'`);\n lines.push(` -d \'${JSON.stringify(body)}\'`);\n }\n if (isMP) lines.push(` -F \'file=@/path/to/file\'`);\n return lines.join(\" \\\\\\n\");\n }\n if (lang === \"javascript\") {\n const hdrs = `\'X-API-Key\': \'YOUR_API_KEY\'${hasBody ? \",\\n \'Content-Type\': \'application/json\'\" : \"\"}`;\n const opts = [` method: \'${M}\'`, ` headers: {\\n ${hdrs}\\n }`];\n if (hasBody)\n opts.push(\n ` body: JSON.stringify(${bodyStr.split(\"\\n\").join(\"\\n \")})`,\n );\n return `const res = await fetch(\'${url}${qStr}\', {\\n${opts.join(\",\\n\")}\\n});\\nconst data = await res.json();\\nconsole.log(data);`;\n }\n if (lang === \"python\") {\n const pyBody = bodyStr\n ? bodyStr\n .replace(/null/g, \"None\")\n .replace(/true/g, \"True\")\n .replace(/false/g, \"False\")\n : null;\n return [\n `import requests`,\n ``,\n `url = \"${url}${qStr}\"`,\n `headers = {\\n \"X-API-Key\": \"YOUR_API_KEY\",${hasBody ? \'\\n \"Content-Type\": \"application/json\",\' : \"\"}`,\n `}`,\n ...(pyBody ? [``, `payload = ${pyBody}`, ``] : [``]),\n `response = requests.${method}(url, headers=headers${hasBody ? \", json=payload\" : isMP ? \', files={\"file\": open(\"/path/to/file\",\"rb\")}\' : \"\"})`,\n `print(response.json())`,\n ].join(\"\\n\");\n }\n if (lang === \"php\") {\n const hdrs = `\"X-API-Key: \" . $apiKey${hasBody ? \',\\n \"Content-Type: application/json\"\' : \"\"}`;\n return [\n `<?php`,\n ``,\n `$url = \"${url}${qStr}\";`,\n `$apiKey = \"YOUR_API_KEY\";`,\n ``,\n `$ch = curl_init($url);`,\n `curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);`,\n `curl_setopt($ch, CURLOPT_CUSTOMREQUEST, \"${M}\");`,\n `curl_setopt($ch, CURLOPT_HTTPHEADER, [\\n ${hdrs}\\n]);`,\n ...(hasBody\n ? [\n ``,\n `$payload = json_encode(${bodyStr});`,\n `curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);`,\n ]\n : []),\n ``,\n `$response = curl_exec($ch);`,\n `curl_close($ch);`,\n ``,\n `$data = json_decode($response, true);`,\n `print_r($data);`,\n ].join(\"\\n\");\n }\n if (lang === \"go\") {\n const imp = hasBody\n ? `\"bytes\"\\n\\t\"encoding/json\"\\n\\t\"fmt\"\\n\\t\"io\"\\n\\t\"net/http\"`\n : `\"fmt\"\\n\\t\"io\"\\n\\t\"net/http\"`;\n const bLines = hasBody\n ? [\n `\\tpayload := map[string]interface{}{`,\n ...Object.entries(body || {}).map(\n ([k, v]) => `\\t\\t\"${k}\": ${JSON.stringify(v)},`,\n ),\n `\\t}`,\n `\\tjsonData, _ := json.Marshal(payload)`,\n `\\treq, _ := http.NewRequest(\"${M}\", url, bytes.NewBuffer(jsonData))`,\n ]\n : [`\\treq, _ := http.NewRequest(\"${M}\", url, nil)`];\n return [\n `package main`,\n ``,\n `import (\\n\\t${imp}\\n)`,\n ``,\n `func main() {`,\n `\\turl := \"${url}${qStr}\"`,\n ``,\n ...bLines,\n `\\treq.Header.Set(\"X-API-Key\", \"YOUR_API_KEY\")`,\n ...(hasBody\n ? [`\\treq.Header.Set(\"Content-Type\", \"application/json\")`]\n : []),\n ``,\n `\\tclient := &http.Client{}`,\n `\\tresp, err := client.Do(req)`,\n `\\tif err != nil { panic(err) }`,\n `\\tdefer resp.Body.Close()`,\n ``,\n `\\tbody, _ := io.ReadAll(resp.Body)`,\n `\\tfmt.Println(string(body))`,\n `}`,\n ].join(\"\\n\");\n }\n if (lang === \"rust\") {\n const bLines = hasBody\n ? [\n ` let payload = serde_json::json!(${bodyStr});`,\n ``,\n ` let res = client.${method}(&url)`,\n ` .header(\"X-API-Key\", \"YOUR_API_KEY\")`,\n ` .json(&payload)`,\n ` .send().await?;`,\n ]\n : [\n ` let res = client.${method}(&url)`,\n ` .header(\"X-API-Key\", \"YOUR_API_KEY\")`,\n ` .send().await?;`,\n ];\n return [\n `use reqwest;`,\n ``,\n `#[tokio::main]`,\n `async fn main() -> Result<(), Box<dyn std::error::Error>> {`,\n ` let client = reqwest::Client::new();`,\n ` let url = \"${url}${qStr}\";`,\n ``,\n ...bLines,\n ``,\n ` let data: serde_json::Value = res.json().await?;`,\n ` println!(\"{:#?}\", data);`,\n ``,\n ` Ok(())`,\n `}`,\n ].join(\"\\n\");\n }\n return \"\";\n }\n\n function CopyBtn({ text }) {\n const [ok, setOk] = useState(false);\n const copy = () => {\n navigator.clipboard.writeText(text).catch(() => {});\n setOk(true);\n setTimeout(() => setOk(false), 1800);\n };\n return h(\n \"button\",\n { onClick: copy, className: `copy-btn${ok ? \" ok\" : \"\"}` },\n h(\"i\", { className: `fa ${ok ? \"fa-check\" : \"fa-copy\"}` }),\n ok ? \"Copied!\" : \"Copy\",\n );\n }\n\n function CodeEditor({ value, onChange }) {\n const editorRef = useRef(null);\n const codeMirrorRef = useRef(null);\n const [error, setError] = useState(null);\n\n const validateJson = (text) => {\n if (!text || text.trim() === \"\") {\n setError(null);\n return null;\n }\n try {\n JSON.parse(text);\n setError(null);\n return null;\n } catch (e) {\n const match = e.message.match(/position (\\d+)/);\n const pos = match ? parseInt(match[1]) : null;\n const errorInfo = {\n message: e.message,\n position: pos,\n };\n setError(errorInfo);\n return errorInfo;\n }\n };\n\n const markError = (err) => {\n if (!codeMirrorRef.current) return;\n const cm = codeMirrorRef.current;\n cm.clearGutter(\"error-gutter\");\n cm.getAllMarks().forEach((m) => m.clear());\n\n if (err && err.position !== null) {\n const pos = cm.posFromIndex(err.position);\n if (pos) {\n cm.markText(\n { line: pos.line, ch: pos.ch },\n { line: pos.line, ch: pos.ch + 1 },\n {\n className: \"cm-json-error\",\n attributes: { title: err.message },\n },\n );\n cm.setGutterMarker(\n pos.line,\n \"error-gutter\",\n (() => {\n const marker = document.createElement(\"div\");\n marker.style.color = \"#e05c4b\";\n marker.innerHTML = \"\u{25cf}\";\n marker.title = err.message;\n return marker;\n })(),\n );\n }\n }\n };\n\n useEffect(() => {\n if (!editorRef.current) return;\n\n codeMirrorRef.current = window.CodeMirror.fromTextArea(\n editorRef.current,\n {\n mode: { name: \"javascript\", json: true },\n theme: \"atom-one-dark\",\n lineNumbers: false,\n gutters: [\"error-gutter\"],\n autoCloseBrackets: true,\n matchBrackets: true,\n indentUnit: 2,\n tabSize: 2,\n lineWrapping: true,\n viewportMargin: Infinity,\n extraKeys: {\n Tab: (cm) => {\n if (cm.somethingSelected()) {\n cm.indentSelection(\"add\");\n } else {\n cm.replaceSelection(\" \", \"end\");\n }\n },\n },\n },\n );\n\n codeMirrorRef.current.on(\"change\", (instance) => {\n const newValue = instance.getValue();\n onChange(newValue);\n const err = validateJson(newValue);\n markError(err);\n });\n\n return () => {\n if (codeMirrorRef.current) {\n codeMirrorRef.current.toTextArea();\n codeMirrorRef.current = null;\n }\n };\n }, []);\n\n useEffect(() => {\n if (\n codeMirrorRef.current &&\n value !== codeMirrorRef.current.getValue()\n ) {\n codeMirrorRef.current.setValue(value);\n }\n }, [value]);\n\n return h(\n \"div\",\n null,\n h(\n \"div\",\n { className: \"code-editor-wrapper\" },\n h(\"textarea\", { ref: editorRef, defaultValue: value }),\n ),\n error &&\n h(\n \"div\",\n { className: \"json-error-hint\" },\n h(\"i\", {\n className: \"fa fa-circle-exclamation\",\n }),\n `Invalid JSON: ${error.message}`,\n ),\n );\n }\n\n function HljsCode({ code, lang }) {\n const ref = useRef(null);\n useEffect(() => {\n if (!ref.current || !window.hljs) return;\n ref.current.removeAttribute(\"data-highlighted\");\n ref.current.textContent = code;\n window.hljs.highlightElement(ref.current);\n }, [code, lang]);\n return h(\"code\", { ref, className: `language-${lang}` });\n }\n\n function CodeBlock({ code, lang, label }) {\n return h(\n \"div\",\n { className: \"code-wrap\" },\n h(\n \"div\",\n { className: \"code-hd\" },\n h(\"span\", { className: \"code-hd-lbl\" }, label),\n h(CopyBtn, { text: code }),\n ),\n h(\"pre\", null, h(HljsCode, { code, lang })),\n );\n }\n\n function MethodBadge({ method }) {\n return h(\n \"span\",\n { className: `method-badge m-${method} mb-${method}` },\n method.toUpperCase(),\n );\n }\n\n function ParamsTable({ params }) {\n if (!params?.length) return null;\n return h(\n \"div\",\n { style: { marginBottom: 20 } },\n h(\"div\", { className: \"sec-lbl\" }, \"Parameters\"),\n h(\n \"div\",\n { className: \"params-tbl\" },\n params.map((p, i) =>\n h(\n \"div\",\n { key: i, className: \"p-row\" },\n h(\n \"div\",\n { className: \"p-name\" },\n p.name,\n p.required && h(\"span\", { className: \"p-req\" }, \" *\"),\n ),\n h(\n \"div\",\n { className: \"p-meta\" },\n h(\"span\", { className: \"p-in\" }, p.in),\n p.schema?.type &&\n h(\n \"span\",\n { className: \"p-type\" },\n Array.isArray(p.schema.type)\n ? p.schema.type[0]\n : p.schema.type,\n ),\n ),\n h(\n \"div\",\n { className: \"p-desc\" },\n p.description || \"\",\n p.example !== undefined &&\n h(\n \"div\",\n {\n style: {\n fontSize: 11,\n color: \"var(--t3)\",\n marginTop: 2,\n },\n },\n `e.g. \"${p.example}\"`,\n ),\n p.schema?.enum &&\n h(\n \"div\",\n { style: { marginTop: 5 } },\n p.schema.enum.map((v) =>\n h(\n \"span\",\n {\n key: v,\n className: \"enum-chip\",\n },\n v,\n ),\n ),\n ),\n p.schema?.default !== undefined &&\n h(\n \"div\",\n {\n style: {\n fontSize: 11,\n color: \"var(--t3)\",\n marginTop: 2,\n },\n },\n `default: ${p.schema.default}`,\n ),\n ),\n ),\n ),\n ),\n );\n }\n\n function RequestBodyPanel({ op, spec }) {\n if (!op.requestBody) return null;\n\n // Handle requestBody $ref at the top level\n let requestBody = op.requestBody;\n if (op.requestBody.$ref) {\n requestBody = resolveRef(op.requestBody.$ref, spec) || op.requestBody;\n }\n\n const content = requestBody.content || {};\n const isMP = !!content[\"multipart/form-data\"];\n const jsonCt = content[\"application/json\"];\n\n // Get schema - could be directly on jsonCt or nested\n let schema = null;\n if (jsonCt) {\n if (jsonCt.schema?.$ref) {\n schema = resolveRef(jsonCt.schema.$ref, spec);\n } else if (jsonCt.schema) {\n schema = jsonCt.schema;\n }\n }\n\n const example = getBodyEx(op, spec);\n\n // Handle allOf composition by merging properties\n if (schema?.allOf && Array.isArray(schema.allOf)) {\n const merged = { properties: {}, required: [] };\n schema.allOf.forEach((s) => {\n const resolved = s.$ref ? resolveRef(s.$ref, spec) : s;\n if (resolved?.properties) {\n Object.assign(merged.properties, resolved.properties);\n }\n if (resolved?.required) {\n merged.required.push(...resolved.required);\n }\n });\n schema = { ...schema, ...merged };\n }\n\n // Handle oneOf/anyOf by using the first schema as a hint\n if (!schema?.properties && (schema?.oneOf || schema?.anyOf)) {\n const first = schema.oneOf?.[0] || schema.anyOf?.[0];\n if (first) {\n schema = first.$ref ? resolveRef(first.$ref, spec) : first;\n }\n }\n\n // If schema is still just a $ref, resolve it\n if (schema && !schema.properties && schema.$ref) {\n schema = resolveRef(schema.$ref, spec) || schema;\n }\n\n return h(\n \"div\",\n { style: { marginBottom: 20 } },\n h(\n \"div\",\n { className: \"rb-hd\" },\n h(\n \"div\",\n {\n className: \"sec-lbl\",\n style: { marginBottom: 0 },\n },\n \"Request Body\",\n ),\n requestBody.required &&\n h(\"span\", { className: \"rb-req\" }, \"required\"),\n ),\n isMP &&\n h(\n \"div\",\n { className: \"mp-note\" },\n h(\"i\", {\n className: \"fa fa-paperclip\",\n style: {\n color: \"var(--orange)\",\n marginRight: 6,\n },\n }),\n \"multipart/form-data \u{2014} send files as form data\",\n ),\n schema?.properties &&\n h(\n \"div\",\n {\n className: \"params-tbl\",\n style: { marginBottom: 12 },\n },\n Object.entries(schema.properties).map(([name, prop]) => {\n const r = prop.$ref\n ? resolveRef(prop.$ref, spec) || prop\n : prop;\n const t = Array.isArray(r.type) ? r.type[0] : r.type;\n return h(\n \"div\",\n { key: name, className: \"p-row\" },\n h(\n \"div\",\n { className: \"p-name\" },\n name,\n (schema.required || []).includes(name) &&\n h(\"span\", { className: \"p-req\" }, \" *\"),\n ),\n h(\n \"div\",\n { className: \"p-meta\" },\n h(\"span\", { className: \"p-type\" }, t || \"\u{2014}\"),\n ),\n h(\n \"div\",\n { className: \"p-desc\" },\n r.description || \"\",\n r.enum &&\n h(\n \"div\",\n { style: { marginTop: 5 } },\n r.enum.map((v) =>\n h(\n \"span\",\n {\n key: v,\n className: \"enum-chip\",\n },\n v,\n ),\n ),\n ),\n ),\n );\n }),\n ),\n example &&\n h(CodeBlock, {\n code: JSON.stringify(example, null, 2),\n lang: \"json\",\n label: \"Example Request\",\n }),\n );\n }\n\n function ResponsesPanel({ responses }) {\n const codes = Object.keys(responses || {});\n const [active, setActive] = useState(codes[0]);\n if (!codes.length) return null;\n const resp = responses[active];\n const example = resp?.content?.[\"application/json\"]?.example;\n return h(\n \"div\",\n { style: { marginBottom: 20 } },\n h(\"div\", { className: \"sec-lbl\" }, \"Responses\"),\n h(\n \"div\",\n { className: \"resp-tabs\" },\n codes.map((c) =>\n h(\n \"button\",\n {\n key: c,\n onClick: () => setActive(c),\n className: `resp-tab ${active === c ? \"on\" : \"\"} ${sCls(c)}`,\n },\n c,\n ),\n ),\n ),\n h(\n \"div\",\n { className: \"resp-box\" },\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n marginBottom: example ? 12 : 0,\n },\n },\n h(\n \"span\",\n {\n className: sCls(active),\n style: {\n fontFamily: \"monospace\",\n fontWeight: 700,\n fontSize: 15,\n },\n },\n active,\n ),\n h(\"span\", { style: { color: \"var(--t2)\" } }, resp?.description),\n ),\n example &&\n h(CodeBlock, {\n code: JSON.stringify(example, null, 2),\n lang: \"json\",\n label: \"Example Response\",\n }),\n ),\n );\n }\n\n function ResponseDisplay({ response, loading }) {\n const [respTab, setRespTab] = useState(\"body\");\n if (loading) {\n return h(\n \"div\",\n { className: \"response-area\" },\n h(\n \"div\",\n { className: \"response-hd\" },\n h(\n \"div\",\n { className: \"response-ttl\" },\n h(\"i\", { className: \"fa fa-server\" }),\n \"Response\",\n ),\n ),\n h(\n \"div\",\n {\n style: {\n padding: \"40px 0\",\n textAlign: \"center\",\n color: \"var(--t3)\",\n },\n },\n h(\"i\", {\n className: \"fa fa-spinner fa-spin\",\n style: { marginRight: 8 },\n }),\n \"Awaiting response...\",\n ),\n );\n }\n if (!response) {\n return h(\n \"div\",\n { className: \"response-area\" },\n h(\n \"div\",\n { className: \"response-hd\" },\n h(\n \"div\",\n { className: \"response-ttl\" },\n h(\"i\", { className: \"fa fa-server\" }),\n \"Response\",\n ),\n ),\n h(\n \"div\",\n {\n style: {\n padding: \"40px 0\",\n textAlign: \"center\",\n color: \"var(--t3)\",\n background: \"var(--bg-card)\",\n borderRadius: \"8px\",\n border: \"1px dashed var(--bd)\",\n fontSize: \"13px\",\n },\n },\n h(\n \"div\",\n { className: \"empty-response\" },\n h(\"i\", { className: \"fa fa-wifi\", style: { marginRight: 8 } }),\n \"Click \'Send Request\' to test the API\",\n ),\n ),\n );\n }\n\n const statusClass =\n response.status >= 200 && response.status < 300\n ? \"success\"\n : response.status >= 400\n ? \"error\"\n : \"info\";\n\n const responseJson = JSON.stringify(response.data, null, 2);\n\n return h(\n \"div\",\n { className: \"response-area\" },\n h(\n \"div\",\n { className: \"response-hd\" },\n h(\n \"div\",\n { className: \"response-ttl\" },\n h(\"i\", { className: \"fa fa-server\" }),\n \"Response\",\n ),\n h(\n \"span\",\n {\n className: `response-status ${statusClass}`,\n },\n `${response.status} ${response.statusText || \"\"}`,\n ),\n ),\n h(\n \"div\",\n {\n className: \"resp-tabs\",\n style: {\n margin: \"12px 0\",\n padding: \"0 4px\",\n borderBottom: \"1px solid var(--bd-sub)\",\n },\n },\n h(\n \"button\",\n {\n className: `resp-tab ${respTab === \"body\" ? \"on\" : \"\"}`,\n onClick: () => setRespTab(\"body\"),\n style: {\n fontSize: \"11px\",\n padding: \"6px 12px\",\n },\n },\n \"Body\",\n ),\n h(\n \"button\",\n {\n className: `resp-tab ${respTab === \"header\" ? \"on\" : \"\"}`,\n onClick: () => setRespTab(\"header\"),\n style: {\n fontSize: \"11px\",\n padding: \"6px 12px\",\n },\n },\n \"Headers\",\n ),\n ),\n respTab === \"body\"\n ? h(\n \"div\",\n { className: \"code-wrap\", style: { marginBottom: 0 } },\n h(\n \"div\",\n { className: \"code-hd\" },\n h(\"span\", { className: \"code-hd-lbl\" }, \"JSON Response\"),\n h(CopyBtn, { text: responseJson }),\n ),\n h(\n \"pre\",\n { className: \"response-pre\" },\n h(HljsCode, { code: responseJson, lang: \"json\" }),\n ),\n )\n : h(\n \"div\",\n { className: \"code-wrap\", style: { marginBottom: 0 } },\n h(\n \"div\",\n { className: \"code-hd\" },\n h(\"span\", { className: \"code-hd-lbl\" }, \"Response Headers\"),\n h(CopyBtn, {\n text: JSON.stringify(response.headers, null, 2),\n }),\n ),\n h(\n \"pre\",\n { className: \"response-pre\" },\n h(HljsCode, {\n code: JSON.stringify(response.headers, null, 2),\n lang: \"json\",\n }),\n ),\n ),\n );\n }\n\n function CodeSamples({\n ep,\n spec,\n apiKey,\n bearerToken,\n authType,\n baseUrl,\n setApiKey,\n setBearerToken,\n setAuthType,\n showApiKey,\n setShowApiKey,\n showBearer,\n setShowBearer,\n }) {\n const [lang, setLang] = useState(\"curl\");\n const [activeTab, setActiveTab] = useState(\"samples\");\n const code = useMemo(() => genCode(lang, ep, spec), [lang, ep, spec]);\n const info = LANGS.find((l) => l.id === lang);\n const [params, setParams] = useState({});\n const [bodyText, setBodyText] = useState(\"\");\n const [selectedFiles, setSelectedFiles] = useState({});\n const [response, setResponse] = useState(null);\n const [loading, setLoading] = useState(false);\n\n const queryParams = (ep.op.parameters || []).filter(\n (p) => p.in === \"query\",\n );\n const pathParams = (ep.op.parameters || []).filter(\n (p) => p.in === \"path\",\n );\n const bodyExample = getBodyEx(ep.op, spec);\n\n const securityRequirements = ep.op.security || spec.security || [];\n const securitySchemes = spec.components?.securitySchemes || {};\n\n const requiredSchemes = useMemo(() => {\n const schemes = [];\n securityRequirements.forEach((req) => {\n Object.keys(req).forEach((name) => {\n const scheme = securitySchemes[name];\n if (scheme) {\n schemes.push({ name, ...scheme });\n } else if (name === \"bearer_auth\") {\n // Fallback for common naming if not in components\n schemes.push({\n name,\n type: \"http\",\n scheme: \"bearer\",\n });\n } else if (name === \"api_key\") {\n schemes.push({\n name,\n type: \"apiKey\",\n in: \"header\",\n name: \"X-API-Key\",\n });\n }\n });\n });\n return schemes;\n }, [securityRequirements, securitySchemes]);\n\n const hasApiKey = requiredSchemes.some(\n (s) =>\n s.type === \"apiKey\" || (s.type === \"http\" && s.scheme === \"basic\"),\n );\n const hasBearer = requiredSchemes.some(\n (s) =>\n (s.type === \"http\" && s.scheme === \"bearer\") ||\n s.type === \"oauth2\" ||\n s.type === \"openIdConnect\",\n );\n const isSecurityRequired = requiredSchemes.length > 0;\n\n useEffect(() => {\n if (hasApiKey && !hasBearer) setAuthType(\"api_key\");\n else if (hasBearer && !hasApiKey) setAuthType(\"bearer\");\n }, [hasApiKey, hasBearer]);\n\n const multipartContent =\n ep.op.requestBody?.content?.[\"multipart/form-data\"];\n const isMultipart = !!multipartContent;\n const multipartSchema = multipartContent?.schema;\n const fileFields =\n isMultipart && multipartSchema?.properties\n ? Object.entries(multipartSchema.properties).filter(\n ([, prop]) =>\n prop.type === \"string\" && prop.format === \"binary\",\n )\n : [];\n\n useEffect(() => {\n if (bodyExample) {\n setBodyText(JSON.stringify(bodyExample, null, 2));\n }\n }, [ep.id]);\n\n useEffect(() => {\n setParams({});\n setResponse(null);\n setSelectedFiles({});\n }, [ep.id]);\n\n const buildUrl = () => {\n let url = baseUrl + ep.path;\n pathParams.forEach((p) => {\n const val = params[p.name] || `{${p.name}}`;\n url = url.replace(`{${p.name}}`, val);\n });\n const qParams = Object.entries(params).filter(\n ([key]) => !pathParams.find((p) => p.name === key),\n );\n if (qParams.length) {\n url +=\n \"?\" +\n qParams\n .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)\n .join(\"&\");\n }\n return url;\n };\n\n const sendRequest = async () => {\n setLoading(true);\n setResponse(null);\n const url = buildUrl();\n\n if (isMultipart && fileFields.length > 0) {\n const formData = new FormData();\n\n fileFields.forEach(([fieldName]) => {\n const file = selectedFiles[fieldName];\n if (file) {\n formData.append(fieldName, file);\n }\n });\n\n const headers = {};\n if (isSecurityRequired) {\n if (authType === \"api_key\" && apiKey) {\n const scheme = requiredSchemes.find(\n (s) =>\n s.type === \"apiKey\" ||\n (s.type === \"http\" && s.scheme === \"basic\"),\n );\n const headerName = scheme?.name || \"X-API-Key\";\n headers[headerName] = apiKey;\n } else if (authType === \"bearer\" && bearerToken) {\n headers[\"Authorization\"] = `Bearer ${bearerToken}`;\n }\n }\n // Note: Don\'t set Content-Type for FormData, browser sets it with boundary\n\n const options = {\n method: ep.method.toUpperCase(),\n headers,\n body: formData,\n };\n\n try {\n const res = await fetch(url, options);\n const data = await res.json().catch(() => res.text());\n const headers = {};\n res.headers.forEach((v, k) => {\n headers[k] = v;\n });\n setResponse({\n status: res.status,\n statusText: res.statusText,\n data: typeof data === \"string\" ? { raw: data } : data,\n headers,\n });\n } catch (e) {\n setResponse({\n status: 0,\n statusText: \"Network Error\",\n data: {\n error: \"Failed to send request\",\n details: e.message,\n },\n });\n } finally {\n setLoading(false);\n }\n return;\n }\n\n const headers = {\n \"Content-Type\": \"application/json\",\n };\n if (isSecurityRequired) {\n if (authType === \"api_key\" && apiKey) {\n const scheme = requiredSchemes.find(\n (s) =>\n s.type === \"apiKey\" ||\n (s.type === \"http\" && s.scheme === \"basic\"),\n );\n const headerName = scheme?.name || \"X-API-Key\";\n headers[headerName] = apiKey;\n } else if (authType === \"bearer\" && bearerToken) {\n headers[\"Authorization\"] = `Bearer ${bearerToken}`;\n }\n }\n const options = {\n method: ep.method.toUpperCase(),\n headers,\n };\n if (\n ep.method.toLowerCase() !== \"get\" &&\n ep.method.toLowerCase() !== \"head\" &&\n bodyText\n ) {\n try {\n options.body = JSON.parse(bodyText);\n } catch (e) {\n setLoading(false);\n setResponse({\n status: 400,\n statusText: \"Invalid JSON\",\n data: {\n error: \"Request body is not valid JSON\",\n details: e.message,\n },\n });\n return;\n }\n }\n try {\n const res = await fetch(url, options);\n const data = await res.json().catch(() => res.text());\n const headers = {};\n res.headers.forEach((v, k) => {\n headers[k] = v;\n });\n setResponse({\n status: res.status,\n statusText: res.statusText,\n data: typeof data === \"string\" ? { raw: data } : data,\n headers,\n });\n } catch (e) {\n setResponse({\n status: 0,\n statusText: \"Network Error\",\n data: {\n error: \"Failed to send request\",\n details: e.message,\n },\n });\n } finally {\n setLoading(false);\n }\n };\n\n const updateParam = (name, value) => {\n setParams((prev) => ({ ...prev, [name]: value }));\n };\n\n const statusClass = response\n ? response.status >= 200 && response.status < 300\n ? \"success\"\n : response.status >= 400\n ? \"error\"\n : \"info\"\n : \"\";\n return h(\n \"div\",\n { style: { marginBottom: 20 } },\n h(\"div\", { className: \"sec-lbl\" }, \"Code Samples\"),\n h(\n \"div\",\n { className: \"test-tabs\", style: { marginBottom: 12 } },\n h(\n \"button\",\n {\n className: `test-tab ${activeTab === \"samples\" ? \"on\" : \"\"}`,\n onClick: () => setActiveTab(\"samples\"),\n },\n h(\"i\", {\n className: \"fa fa-code test-tab-icon\",\n }),\n \"Samples\",\n ),\n h(\n \"button\",\n {\n className: `test-tab ${activeTab === \"tryit\" ? \"on\" : \"\"}`,\n onClick: () => setActiveTab(\"tryit\"),\n },\n h(\"i\", {\n className: \"fa fa-flask test-tab-icon\",\n }),\n \"Try It\",\n ),\n ),\n activeTab === \"samples\" &&\n h(\n \"div\",\n null,\n h(\n \"div\",\n { className: \"code-wrap\" },\n h(\n \"div\",\n { className: \"lang-tabs\" },\n LANGS.map((l) =>\n h(\n \"button\",\n {\n key: l.id,\n onClick: () => setLang(l.id),\n className: `lang-tab ${lang === l.id ? \"on\" : \"\"}`,\n },\n h(\"i\", {\n className: l.fa,\n style: { fontSize: 13 },\n }),\n l.label,\n ),\n ),\n ),\n h(\n \"div\",\n { className: \"code-hd\" },\n h(\n \"span\",\n { className: \"code-hd-lbl\" },\n `${info?.label} example`,\n ),\n h(CopyBtn, { text: code }),\n ),\n h(\n \"pre\",\n null,\n h(HljsCode, {\n code,\n lang: info?.hl || \"bash\",\n }),\n ),\n ),\n ),\n activeTab === \"tryit\" &&\n h(\n \"div\",\n { className: \"test-panel\" },\n h(\n \"div\",\n { className: \"test-panel-hd\" },\n h(\n \"div\",\n { className: \"test-panel-ttl\" },\n h(\"i\", { className: \"fa fa-flask\" }),\n \"Test This Endpoint\",\n ),\n h(\n \"button\",\n {\n className: `btn-test ${loading ? \"loading\" : \"\"}`,\n onClick: sendRequest,\n disabled: loading,\n },\n h(\"i\", {\n className: loading\n ? \"fa fa-spinner fa-spin\"\n : \"fa fa-paper-plane\",\n }),\n loading ? \"Sending...\" : \"Send Request\",\n ),\n ),\n h(\n \"div\",\n { className: \"test-url\" },\n h(\n \"span\",\n {\n className: `test-method m-${ep.method} mb-${ep.method}`,\n },\n ep.method.toUpperCase(),\n ),\n buildUrl(),\n ),\n h(\n \"div\",\n { style: { marginBottom: 12 } },\n [...pathParams, ...queryParams].length > 0\n ? [...pathParams, ...queryParams].map((p) =>\n h(\n \"div\",\n {\n key: p.name,\n className: \"param-input-row\",\n },\n h(\n \"div\",\n { className: \"param-label\" },\n p.name,\n p.required &&\n h(\n \"span\",\n {\n style: {\n color: \"var(--red)\",\n },\n },\n \" *\",\n ),\n ),\n h(\"input\", {\n className: \"param-input\",\n type: \"text\",\n placeholder: p.example || p.description || \"\",\n value: params[p.name] || \"\",\n onChange: (e) => updateParam(p.name, e.target.value),\n }),\n h(\n \"div\",\n { className: \"param-type\" },\n Array.isArray(p.schema?.type)\n ? p.schema.type.join(\" | \")\n : p.schema?.type || \"string\",\n ),\n ),\n )\n : h(\n \"div\",\n {\n style: {\n color: \"var(--t3)\",\n fontSize: 12,\n textAlign: \"center\",\n padding: \"20px 0\",\n },\n },\n h(\"i\", {\n className: \"fa fa-info-circle\",\n style: { marginRight: 6 },\n }),\n \"No parameters for this endpoint\",\n ),\n ),\n isSecurityRequired &&\n h(\n \"div\",\n { className: \"auth-header-input\" },\n h(\n \"div\",\n {\n className: \"auth-header-label\",\n style: {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n width: \"100%\",\n },\n },\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n },\n },\n h(\"i\", { className: \"fa fa-key\" }),\n \"Authorization\",\n ),\n hasApiKey &&\n hasBearer &&\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n gap: 4,\n },\n },\n h(\n \"button\",\n {\n className: `theme-btn ${authType === \"api_key\" ? \"on\" : \"\"}`,\n style: {\n width: \"auto\",\n padding: \"4px 10px\",\n fontSize: \"11px\",\n },\n onClick: () => setAuthType(\"api_key\"),\n title: \"API Key authentication\",\n },\n \"API Key\",\n ),\n h(\n \"button\",\n {\n className: `theme-btn ${authType === \"bearer\" ? \"on\" : \"\"}`,\n style: {\n width: \"auto\",\n padding: \"4px 10px\",\n fontSize: \"11px\",\n },\n onClick: () => setAuthType(\"bearer\"),\n title: \"Bearer Token authentication\",\n },\n \"Bearer\",\n ),\n ),\n ),\n authType === \"api_key\" && hasApiKey\n ? h(\n \"div\",\n {\n style: { marginTop: \"8px\", position: \"relative\" },\n },\n h(\"input\", {\n type: showApiKey ? \"text\" : \"password\",\n placeholder: apiKey ? `API key set` : \"Enter API key\",\n value: apiKey,\n onChange: (e) => setApiKey(e.target.value),\n style: {\n width: \"100%\",\n paddingRight: \"30px\",\n marginBottom: apiKey ? \"4px\" : \"0\",\n },\n }),\n h(\"i\", {\n className: `fa ${showApiKey ? \"fa-eye-slash\" : \"fa-eye\"}`,\n onClick: () => setShowApiKey(!showApiKey),\n style: {\n position: \"absolute\",\n right: \"10px\",\n top: \"10px\",\n fontSize: \"12px\",\n color: \"var(--t3)\",\n cursor: \"pointer\",\n },\n }),\n apiKey &&\n h(\n \"div\",\n {\n style: {\n fontSize: \"10px\",\n color: \"var(--green)\",\n display: \"flex\",\n alignItems: \"center\",\n gap: \"4px\",\n },\n },\n h(\"i\", {\n className: \"fa fa-check-circle\",\n }),\n \"API key stored in memory\",\n ),\n )\n : authType === \"bearer\" && hasBearer\n ? h(\n \"div\",\n {\n style: { marginTop: \"8px\", position: \"relative\" },\n },\n h(\"input\", {\n type: showBearer ? \"text\" : \"password\",\n placeholder: bearerToken\n ? `Token set`\n : \"Enter Bearer token\",\n value: bearerToken,\n onChange: (e) => setBearerToken(e.target.value),\n style: {\n width: \"100%\",\n paddingRight: \"30px\",\n marginBottom: bearerToken ? \"4px\" : \"0\",\n },\n }),\n h(\"i\", {\n className: `fa ${showBearer ? \"fa-eye-slash\" : \"fa-eye\"}`,\n onClick: () => setShowBearer(!showBearer),\n style: {\n position: \"absolute\",\n right: \"10px\",\n top: \"10px\",\n fontSize: \"12px\",\n color: \"var(--t3)\",\n cursor: \"pointer\",\n },\n }),\n bearerToken &&\n h(\n \"div\",\n {\n style: {\n fontSize: \"10px\",\n color: \"var(--green)\",\n display: \"flex\",\n alignItems: \"center\",\n gap: \"4px\",\n },\n },\n h(\"i\", {\n className: \"fa fa-check-circle\",\n }),\n \"Token stored in memory\",\n ),\n )\n : null,\n ),\n // Show file inputs for multipart/form-data\n isMultipart && fileFields.length > 0\n ? h(\n \"div\",\n { style: { marginBottom: 12 } },\n h(\n \"div\",\n {\n style: {\n fontSize: 11,\n fontWeight: 600,\n color: \"var(--t3)\",\n textTransform: \"uppercase\",\n letterSpacing: \"0.08em\",\n marginBottom: 8,\n },\n },\n \"File Upload\",\n ),\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"10px\",\n },\n },\n fileFields.map(([fieldName, prop]) =>\n h(\n \"div\",\n {\n key: fieldName,\n style: {\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"4px\",\n },\n },\n h(\n \"label\",\n {\n style: {\n fontSize: 12,\n color: \"var(--t2)\",\n fontWeight: 500,\n },\n },\n fieldName,\n prop.description &&\n h(\n \"span\",\n {\n style: {\n color: \"var(--t3)\",\n fontWeight: 400,\n marginLeft: \"4px\",\n },\n },\n \" - \" + prop.description,\n ),\n ),\n h(\"input\", {\n type: \"file\",\n className: \"param-input\",\n style: {\n padding: \"6px 10px\",\n cursor: \"pointer\",\n },\n onChange: (e) => {\n const file = e.target.files?.[0];\n if (file) {\n setSelectedFiles((prev) => ({\n ...prev,\n [fieldName]: file,\n }));\n }\n },\n }),\n selectedFiles[fieldName] &&\n h(\n \"div\",\n {\n style: {\n fontSize: 11,\n color: \"var(--green)\",\n },\n },\n h(\"i\", {\n className: \"fa fa-check-circle\",\n style: {\n marginRight: 4,\n },\n }),\n selectedFiles[fieldName].name +\n \" (\" +\n (selectedFiles[fieldName].size / 1024).toFixed(\n 2,\n ) +\n \" KB)\",\n ),\n ),\n ),\n ),\n )\n : // Show JSON editor for non-multipart requests\n (ep.method.toLowerCase() === \"post\" ||\n ep.method.toLowerCase() === \"put\" ||\n ep.method.toLowerCase() === \"patch\") &&\n bodyExample &&\n h(\n \"div\",\n { style: { marginBottom: 12 } },\n h(\n \"div\",\n {\n style: {\n fontSize: 11,\n fontWeight: 600,\n color: \"var(--t3)\",\n textTransform: \"uppercase\",\n letterSpacing: \"0.08em\",\n marginBottom: 8,\n },\n },\n \"Request Body\",\n ),\n h(CodeEditor, {\n value: bodyText,\n onChange: setBodyText,\n }),\n ),\n h(ResponseDisplay, { response, loading }),\n ),\n );\n }\n\n function EndpointDetail({\n ep,\n spec,\n apiKey,\n bearerToken,\n authType,\n baseUrl,\n setApiKey,\n setBearerToken,\n setAuthType,\n showApiKey,\n setShowApiKey,\n showBearer,\n setShowBearer,\n }) {\n if (!ep)\n return h(\n \"div\",\n { className: \"empty\" },\n h(\"i\", { className: \"fa fa-book-open\" }),\n h(\"p\", null, \"Select an endpoint to view details\"),\n );\n const { path, method, op } = ep;\n return h(\n \"div\",\n { className: \"fu\", key: ep.id },\n h(\n \"div\",\n { className: `ep-hdr` },\n h(\n \"div\",\n { className: \"ep-hdr-row\" },\n\n h(\n \"div\",\n { style: { flex: 1, minWidth: 0 } },\n h(\"div\", { className: \"ep-summary\" }, op.summary),\n op.description &&\n h(\n \"p\",\n {\n style: {\n fontSize: 13,\n color: \"var(--t2)\",\n marginTop: 4,\n },\n },\n op.description,\n ),\n h(\"code\", { className: \"ep-path-pill\" }, path),\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n flexWrap: \"wrap\",\n marginTop: 10,\n },\n },\n h(MethodBadge, { method }),\n\n (op.security || spec.security) &&\n h(\n \"span\",\n { className: \"auth-pill\" },\n h(\"i\", {\n className: \"fa fa-key\",\n style: { fontSize: 9 },\n }),\n \" Auth required\",\n ),\n ),\n ),\n ),\n ),\n h(\n \"div\",\n { className: \"ep-body\" },\n h(ParamsTable, { params: op.parameters }),\n h(RequestBodyPanel, { op, spec }),\n h(CodeSamples, {\n ep,\n spec,\n apiKey,\n bearerToken,\n authType,\n baseUrl,\n setApiKey,\n setBearerToken,\n setAuthType,\n showApiKey,\n setShowApiKey,\n showBearer,\n setShowBearer,\n }),\n h(ResponsesPanel, { responses: op.responses }),\n ),\n );\n }\n\n function Sidebar({\n spec,\n endpoints,\n activeId,\n onSelect,\n search,\n onSearch,\n theme,\n onTheme,\n }) {\n const grouped = useMemo(\n () => groupByTag(endpoints, spec.tags),\n [endpoints, spec.tags],\n );\n const [expandedTag, setExpandedTag] = useState(null);\n const [showThemeMenu, setShowThemeMenu] = useState(false);\n\n useEffect(() => {\n if (!showThemeMenu) return;\n const handleClickOutside = () => setShowThemeMenu(false);\n document.addEventListener(\"click\", handleClickOutside);\n return () =>\n document.removeEventListener(\"click\", handleClickOutside);\n }, [showThemeMenu]);\n\n useEffect(() => {\n if (activeId) {\n for (const [tag, eps] of Object.entries(grouped)) {\n if (eps.find((ep) => ep.id === activeId)) {\n setExpandedTag(tag);\n break;\n }\n }\n } else if (Object.keys(grouped).length > 0) {\n setExpandedTag(Object.keys(grouped)[0]);\n }\n }, [activeId, grouped]);\n\n const toggleTag = (tag) => {\n setExpandedTag(expandedTag === tag ? null : tag);\n };\n\n const themeOptions = [\n { value: \"light\", icon: \"fa-sun\", label: \"Light\" },\n { value: \"dark\", icon: \"fa-moon\", label: \"Dark\" },\n {\n value: \"system\",\n icon: \"fa-circle-half-stroke\",\n label: \"System\",\n },\n ];\n\n const currentTheme =\n themeOptions.find((t) => t.value === theme) || themeOptions[0];\n return h(\n Fragment,\n null,\n h(\n \"div\",\n { className: \"sb-hdr\" },\n h(\n \"div\",\n { className: \"sb-brand\" },\n spec.info?.[\"x-logo\"]\n ? h(\"img\", {\n className: \"sb-logo sb-logo-img\",\n src: spec.info[\"x-logo\"],\n alt: spec.info?.title || \"Logo\",\n style: {\n objectFit: \"contain\",\n padding: \"0\",\n background: \"transparent\",\n },\n })\n : h(\n \"div\",\n { className: \"sb-title\" },\n spec.info?.title || \"API Docs\",\n ),\n ),\n h(\n \"div\",\n {\n className: \"sb-themes\",\n style: { position: \"relative\" },\n },\n h(\n \"button\",\n {\n className: \"theme-btn\",\n onClick: (e) => {\n e.stopPropagation();\n setShowThemeMenu(!showThemeMenu);\n },\n title: `${currentTheme.label} theme (click to change)`,\n style: {\n display: \"flex\",\n alignItems: \"center\",\n gap: \"1px\",\n },\n },\n h(\"i\", {\n className: `fa ${currentTheme.icon}`,\n }),\n ),\n showThemeMenu &&\n h(\n \"div\",\n {\n className: \"theme-dropdown\",\n style: {\n position: \"absolute\",\n top: \"100%\",\n right: 0,\n marginTop: \"4px\",\n background: \"var(--bg-card)\",\n border: \"1px solid var(--bd)\",\n borderRadius: \"6px\",\n boxShadow: \"var(--shadow-md)\",\n zIndex: 100,\n minWidth: \"120px\",\n overflow: \"hidden\",\n },\n onClick: (e) => e.stopPropagation(),\n },\n themeOptions.map((opt) =>\n h(\n \"button\",\n {\n key: opt.value,\n className: \"theme-dropdown-item\",\n onClick: () => {\n onTheme(opt.value);\n setShowThemeMenu(false);\n },\n style: {\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n padding: \"8px 12px\",\n width: \"100%\",\n background:\n theme === opt.value\n ? \"var(--accent-bg)\"\n : \"transparent\",\n border: \"none\",\n borderBottom:\n theme === opt.value\n ? \"none\"\n : \"1px solid var(--bd-sub)\",\n color:\n theme === opt.value ? \"var(--accent)\" : \"var(--t2)\",\n fontSize: \"12px\",\n cursor: \"pointer\",\n transition: \"all 0.15s\",\n textAlign: \"left\",\n },\n },\n h(\"i\", {\n className: `fa ${opt.icon}`,\n style: {\n fontSize: \"12px\",\n width: \"14px\",\n },\n }),\n h(\n \"span\",\n {\n style: {\n fontSize: \"12px\",\n fontWeight: theme === opt.value ? 600 : 400,\n },\n },\n opt.label,\n ),\n theme === opt.value &&\n h(\"i\", {\n className: \"fa fa-check\",\n style: {\n marginLeft: \"auto\",\n fontSize: \"11px\",\n },\n }),\n ),\n ),\n ),\n ),\n ),\n h(\n \"div\",\n { className: \"sb-srch\" },\n h(\"i\", { className: \"fa fa-magnifying-glass\" }),\n h(\"input\", {\n type: \"text\",\n placeholder: \"Search....\",\n value: search,\n onChange: (e) => onSearch(e.target.value),\n }),\n ),\n h(\n \"div\",\n { className: \"sb-nav\" },\n Object.entries(grouped).map(([tag, eps]) => {\n const filtered = eps.filter(\n (ep) =>\n !search ||\n ep.path.toLowerCase().includes(search.toLowerCase()) ||\n (ep.op.summary || \"\")\n .toLowerCase()\n .includes(search.toLowerCase()),\n );\n if (!filtered.length) return null;\n const isExpanded = expandedTag === tag;\n return h(\n \"div\",\n { key: tag },\n h(\n \"div\",\n {\n className: \"sb-grp-hd\",\n onClick: () => toggleTag(tag),\n },\n h(\n \"span\",\n {\n style: {\n flex: 1,\n textTransform: \"capitalize\",\n fontSize: 16,\n },\n },\n tag,\n ),\n h(\"i\", {\n className: isExpanded\n ? \"fa fa-chevron-down\"\n : \"fa fa-chevron-right\",\n style: {\n fontSize: \"10px\",\n color: \"var(--t3)\",\n },\n }),\n ),\n isExpanded &&\n filtered.map((ep) =>\n h(\n \"a\",\n {\n key: ep.id,\n href: `#${ep.id}`,\n className: `ep-btn ${activeId === ep.id ? \"on\" : \"\"}`,\n onClick: (e) => {\n e.preventDefault();\n onSelect(ep);\n },\n },\n h(\n \"span\",\n {\n className: `ep-m m-${ep.method}`,\n },\n ep.method.toUpperCase(),\n ),\n h(\n \"span\",\n { className: \"ep-p\" },\n ep.op.summary || ep.path,\n ),\n ),\n ),\n );\n }),\n ),\n h(\n \"div\",\n { className: \"sb-foot\" },\n h(\"span\", null, `${endpoints.length} endpoints`),\n h(\n \"span\",\n {\n style: {\n display: \"flex\",\n alignItems: \"center\",\n gap: 5,\n },\n },\n h(\"i\", {\n className: \"fa fa-circle-dot\",\n style: { color: \"var(--green)\", fontSize: 9 },\n }),\n \"OpenAPI \",\n spec.openapi,\n ),\n ),\n );\n }\n\n function EmptyState({ onImportClick }) {\n const loadSampleData = () => {\n try {\n // Use embedded sample data if available\n if (typeof SAMPLE_DATA !== \"undefined\" && SAMPLE_DATA) {\n const spec =\n typeof SAMPLE_DATA === \"string\"\n ? JSON.parse(SAMPLE_DATA)\n : SAMPLE_DATA;\n onImportClick(spec);\n } else {\n console.error(\"No sample data available\");\n }\n } catch (error) {\n console.error(\"Failed to load sample data:\", error);\n }\n };\n\n return h(\n \"div\",\n {\n className: \"empty\",\n style: {\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"100%\",\n gap: \"16px\",\n },\n },\n h(\"i\", {\n className: \"fa fa-book-open\",\n style: { fontSize: \"48px\", opacity: \"0.2\" },\n }),\n h(\n \"div\",\n { style: { textAlign: \"center\", maxWidth: \"400px\" } },\n h(\n \"h2\",\n {\n style: {\n fontSize: \"18px\",\n fontWeight: \"700\",\n color: \"var(--t1)\",\n marginBottom: \"8px\",\n },\n },\n \"No API Specification Loaded\",\n ),\n h(\n \"p\",\n {\n style: {\n fontSize: \"13px\",\n color: \"var(--t2)\",\n lineHeight: \"1.5\",\n },\n },\n \"Click the button below to load a sample API and explore the documentation features.\",\n ),\n ),\n h(\n \"button\",\n {\n className: \"btn-p\",\n style: {\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n padding: \"10px 20px\",\n width: \"auto\",\n },\n onClick: loadSampleData,\n },\n h(\"i\", { className: \"fa fa-flask\" }),\n \"Try Sample API\",\n ),\n );\n }\n\n function App() {\n // Initialize spec from injected value if available\n let initialSpec = null;\n if (typeof INJECTED_SPEC !== \"undefined\" && INJECTED_SPEC) {\n try {\n initialSpec =\n typeof INJECTED_SPEC === \"string\"\n ? JSON.parse(INJECTED_SPEC)\n : INJECTED_SPEC;\n } catch (e) {\n console.error(\"Failed to parse injected spec:\", e);\n }\n }\n\n const [spec, setSpec] = useState(initialSpec);\n const [search, setSearch] = useState(\"\");\n const endpoints = useMemo(() => (spec ? buildEps(spec) : []), [spec]);\n\n const [activeEp, setActive] = useState(() => {\n const eps = initialSpec ? buildEps(initialSpec) : [];\n const hash = window.location.hash.slice(1);\n if (hash) {\n const ep = eps.find((e) => e.id === hash);\n if (ep) return ep;\n }\n if (eps.length > 0) {\n const grouped = groupByTag(eps, initialSpec?.tags);\n const tagKeys = Object.keys(grouped);\n if (tagKeys.length > 0) {\n return grouped[tagKeys[0]][0];\n }\n return eps[0];\n }\n return null;\n });\n\n const [theme, setTheme] = useState(() => {\n try {\n return localStorage.getItem(\"openapi-ui-theme\") || \"system\";\n } catch {\n return \"system\";\n }\n });\n const [apiKey, setApiKey] = useState(\"\");\n const [showApiKey, setShowApiKey] = useState(false);\n const [bearerToken, setBearerToken] = useState(\"\");\n const [showBearer, setShowBearer] = useState(false);\n const [authType, setAuthType] = useState(\"api_key\");\n const [baseUrl, setBaseUrl] = useState(\"\");\n\n // Initialize baseUrl from spec.servers when spec changes\n useEffect(() => {\n const serverUrl = spec?.servers?.[0]?.url || \"\";\n setBaseUrl(serverUrl);\n }, [spec]);\n\n // Sync active endpoint when endpoints change (e.g. after manual import)\n useEffect(() => {\n if (!endpoints.length) {\n setActive(null);\n return;\n }\n\n const hash = window.location.hash.slice(1);\n if (hash) {\n const ep = endpoints.find((e) => e.id === hash);\n if (ep) {\n setActive(ep);\n return;\n }\n }\n\n // Default to visually first if not already set to a valid endpoint in the current spec\n if (!activeEp || !endpoints.find((e) => e.id === activeEp.id)) {\n const grouped = groupByTag(endpoints, spec?.tags);\n const tagKeys = Object.keys(grouped);\n if (tagKeys.length > 0) {\n setActive(grouped[tagKeys[0]][0]);\n } else {\n setActive(endpoints[0]);\n }\n }\n }, [endpoints]);\n\n // Handle hash navigation\n useEffect(() => {\n const handleHashChange = () => {\n const hash = window.location.hash.slice(1);\n if (hash) {\n const ep = endpoints.find((e) => e.id === hash);\n if (ep && ep.id !== activeEp?.id) {\n setActive(ep);\n }\n } else if (endpoints.length > 0) {\n const grouped = groupByTag(endpoints, spec.tags);\n const firstTag = Object.keys(grouped)[0];\n const firstEp =\n firstTag && grouped[firstTag].length > 0\n ? grouped[firstTag][0]\n : endpoints[0];\n\n if (activeEp?.id !== firstEp.id) {\n setActive(firstEp);\n }\n }\n };\n\n window.addEventListener(\"hashchange\", handleHashChange);\n return () =>\n window.removeEventListener(\"hashchange\", handleHashChange);\n }, [endpoints, activeEp, spec.tags]);\n\n const applyTheme = useCallback((t) => {\n const r =\n t === \"system\"\n ? window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n ? \"dark\"\n : \"light\"\n : t;\n document.documentElement.setAttribute(\"data-theme\", r);\n const d = document.getElementById(\"hljs-dark\");\n const l = document.getElementById(\"hljs-light\");\n if (d) d.disabled = r !== \"dark\";\n if (l) l.disabled = r !== \"light\";\n }, []);\n\n const changeTheme = useCallback(\n (t) => {\n setTheme(t);\n applyTheme(t);\n try {\n localStorage.setItem(\"openapi-ui-theme\", t);\n } catch {}\n },\n [applyTheme],\n );\n\n useEffect(() => {\n // Apply current theme\n applyTheme(theme);\n\n // Setup listener for system changes\n const mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const handler = (e) => {\n if (theme === \"system\") {\n applyTheme(\"system\");\n }\n };\n\n // Initial check isn\'t needed here as applyTheme(theme) runs on every \'theme\' change\n // and theme is initialized correctly.\n\n mq.addEventListener(\"change\", handler);\n return () => mq.removeEventListener(\"change\", handler);\n }, [applyTheme, theme]);\n\n // Clear sensitive auth data on page unload (security measure)\n useEffect(() => {\n const handleBeforeUnload = () => {\n // Credentials are in-memory only, cleared automatically\n console.log(\"Page unloaded - credentials cleared\");\n };\n window.addEventListener(\"beforeunload\", handleBeforeUnload);\n return () =>\n window.removeEventListener(\"beforeunload\", handleBeforeUnload);\n }, []);\n\n // Update hash when active endpoint changes\n useEffect(() => {\n if (activeEp) {\n window.location.hash = activeEp.id;\n }\n }, [activeEp]);\n\n // Update page title based on spec and active endpoint\n useEffect(() => {\n const specTitle = spec?.info?.title || \"API Documentation\";\n if (activeEp) {\n const endpointTitle = activeEp.op.summary || activeEp.path;\n document.title = `${endpointTitle} - ${specTitle}`;\n } else {\n document.title = specTitle;\n }\n }, [activeEp, spec]);\n\n // Show empty state if no spec is loaded\n if (!spec) {\n return h(EmptyState, {\n onImportClick: (sampleSpec) => {\n setSpec(sampleSpec);\n },\n });\n }\n\n return h(\n \"div\",\n { className: \"app\" },\n h(\n \"div\",\n { className: \"sidebar\" },\n h(Sidebar, {\n spec,\n endpoints,\n activeId: activeEp?.id,\n onSelect: setActive,\n search,\n onSearch: setSearch,\n theme,\n onTheme: changeTheme,\n }),\n ),\n h(\n \"div\",\n { className: \"main\" },\n h(\n \"div\",\n { className: \"topbar\" },\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n minWidth: 0,\n overflow: \"hidden\",\n },\n },\n (spec.servers || []).map((s, i) =>\n h(\n \"div\",\n { key: i, className: \"server-pill\" },\n h(\"i\", {\n className: \"fa fa-circle\",\n style: {\n fontSize: 6,\n color: i === 0 ? \"var(--green)\" : \"var(--t4)\",\n },\n }),\n h(\"span\", { className: \"server-url\" }, s.url),\n s.description &&\n h(\n \"span\",\n {\n style: {\n color: \"var(--t3)\",\n fontSize: 11,\n },\n },\n s.description,\n ),\n ),\n ),\n ),\n h(\n \"div\",\n {\n style: {\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n flexShrink: 0,\n },\n },\n h(\n \"div\",\n {\n className: `auth-status ${apiKey ? \"set\" : \"\"}`,\n style: { marginRight: 8 },\n },\n h(\n \"span\",\n { style: { marginLeft: 4 } },\n \"v\" + spec.info?.version,\n ),\n ),\n ),\n ),\n h(\n \"div\",\n { className: \"detail\" },\n h(EndpointDetail, {\n ep: activeEp,\n spec,\n apiKey,\n bearerToken,\n authType,\n baseUrl,\n setApiKey,\n setBearerToken,\n setAuthType,\n showApiKey,\n setShowApiKey,\n showBearer,\n setShowBearer,\n }),\n ),\n ),\n );\n }\n\n createRoot(document.getElementById(\"root\")).render(h(App));\n </script>\n </body>\n</html>\n";Expand description
The raw HTML template used for rendering.