feat: add ApiDoc widget - renders API docs from markdown with sidebar nav, method badges, code copy
This commit is contained in:
parent
d67bd84d4d
commit
d41c386acf
305
bricks/api_doc.js
Normal file
305
bricks/api_doc.js
Normal file
@ -0,0 +1,305 @@
|
||||
var bricks = window.bricks || {};
|
||||
/*
|
||||
* ApiDoc widget - renders API documentation from markdown
|
||||
* Depends on: marked (CDN), highlight.js (CDN, optional)
|
||||
* Options:
|
||||
* md_url: string - URL to fetch markdown from
|
||||
* title: string (optional) - page title
|
||||
* base_url: string (optional) - API base URL for display
|
||||
*/
|
||||
|
||||
bricks.ApiDoc = class extends bricks.VBox {
|
||||
constructor(options) {
|
||||
if (!options) options = {};
|
||||
super(options);
|
||||
this.md_url = bricks.absurl(this.opts.md_url);
|
||||
this.api_title = this.opts.title || 'API Documentation';
|
||||
this.api_base_url = this.opts.base_url || '';
|
||||
this.sections = [];
|
||||
this.active_section = null;
|
||||
this._build_layout();
|
||||
var self = this;
|
||||
schedule_once(function() { self._load(); }, 0.1);
|
||||
}
|
||||
|
||||
_build_layout() {
|
||||
var d = this.dom_element;
|
||||
d.classList.add('bricks-apidoc');
|
||||
d.innerHTML = '';
|
||||
|
||||
// Header
|
||||
var header = document.createElement('div');
|
||||
header.className = 'bricks-apidoc-header';
|
||||
header.innerHTML = '<div class="bricks-apidoc-title">' +
|
||||
this.api_title + '</div>' +
|
||||
(this.api_base_url ? '<div class="bricks-apidoc-baseurl">' +
|
||||
this.api_base_url + '</div>' : '');
|
||||
d.appendChild(header);
|
||||
|
||||
// Body: sidebar + content
|
||||
var body = document.createElement('div');
|
||||
body.className = 'bricks-apidoc-body';
|
||||
d.appendChild(body);
|
||||
|
||||
// Sidebar
|
||||
var sidebar = document.createElement('div');
|
||||
sidebar.className = 'bricks-apidoc-sidebar';
|
||||
body.appendChild(sidebar);
|
||||
this.sidebar_el = sidebar;
|
||||
|
||||
// Content
|
||||
var content = document.createElement('div');
|
||||
content.className = 'bricks-apidoc-content';
|
||||
body.appendChild(content);
|
||||
this.content_el = content;
|
||||
|
||||
// Scroll spy
|
||||
var self = this;
|
||||
content.addEventListener('scroll', function() {
|
||||
self._scroll_spy();
|
||||
});
|
||||
}
|
||||
|
||||
async _load() {
|
||||
if (!this.md_url) return;
|
||||
try {
|
||||
var md_text = await bricks.tget(this.md_url);
|
||||
this._render(md_text);
|
||||
} catch(e) {
|
||||
this.content_el.innerHTML =
|
||||
'<div class="bricks-apidoc-error">Failed to load: ' +
|
||||
this.md_url + '</div>';
|
||||
bricks.error('ApiDoc load error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
_render(md_text) {
|
||||
// Use marked to parse the full markdown
|
||||
var html = marked.parse(md_text);
|
||||
|
||||
// Create a temporary container to parse the DOM
|
||||
var tmp = document.createElement('div');
|
||||
tmp.innerHTML = html;
|
||||
|
||||
// Extract sections (h2 elements = API endpoints)
|
||||
var sections = [];
|
||||
var current = null;
|
||||
var children = tmp.children;
|
||||
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var el = children[i];
|
||||
if (el.tagName === 'H2') {
|
||||
if (current) sections.push(current);
|
||||
current = {
|
||||
heading: el.textContent.trim(),
|
||||
id: 'api-sec-' + sections.length,
|
||||
elements: []
|
||||
};
|
||||
} else if (current) {
|
||||
current.elements.push(el);
|
||||
}
|
||||
}
|
||||
if (current) sections.push(current);
|
||||
|
||||
this.sections = sections;
|
||||
this._build_sidebar();
|
||||
this._build_content(sections);
|
||||
|
||||
// Highlight code blocks
|
||||
if (typeof hljs !== 'undefined') {
|
||||
var blocks = this.content_el.querySelectorAll('pre code');
|
||||
blocks.forEach(function(block) {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
}
|
||||
|
||||
// Add copy buttons to code blocks
|
||||
this._add_copy_buttons();
|
||||
|
||||
// Activate first section
|
||||
if (sections.length > 0) {
|
||||
this._activate_section(sections[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
_build_sidebar() {
|
||||
var sidebar = this.sidebar_el;
|
||||
sidebar.innerHTML = '';
|
||||
|
||||
var self = this;
|
||||
for (var i = 0; i < this.sections.length; i++) {
|
||||
var sec = this.sections[i];
|
||||
var item = document.createElement('div');
|
||||
item.className = 'bricks-apidoc-nav-item';
|
||||
item.setAttribute('data-section', sec.id);
|
||||
|
||||
// Parse method from heading (e.g. "POST /v1/chat/completions")
|
||||
var parts = sec.heading.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
var method = parts[0].toUpperCase();
|
||||
var path = parts.slice(1).join(' ');
|
||||
var badge = document.createElement('span');
|
||||
badge.className = 'bricks-apidoc-method-badge';
|
||||
badge.className += ' bricks-apidoc-method-' + method.toLowerCase();
|
||||
badge.textContent = method;
|
||||
item.appendChild(badge);
|
||||
|
||||
var pathEl = document.createElement('span');
|
||||
pathEl.className = 'bricks-apidoc-nav-path';
|
||||
pathEl.textContent = path.replace('/v1/', '/');
|
||||
item.appendChild(pathEl);
|
||||
} else {
|
||||
item.textContent = sec.heading;
|
||||
}
|
||||
|
||||
item.onclick = (function(sid) {
|
||||
return function() { self._scroll_to(sid); };
|
||||
})(sec.id);
|
||||
|
||||
sidebar.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
_build_content(sections) {
|
||||
var content = this.content_el;
|
||||
content.innerHTML = '';
|
||||
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var sec = sections[i];
|
||||
|
||||
// Section container
|
||||
var section_el = document.createElement('div');
|
||||
section_el.className = 'bricks-apidoc-section';
|
||||
section_el.id = sec.id;
|
||||
content.appendChild(section_el);
|
||||
|
||||
// Section heading with method badge
|
||||
var heading = document.createElement('h2');
|
||||
heading.className = 'bricks-apidoc-section-title';
|
||||
|
||||
var parts = sec.heading.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
var method = parts[0].toUpperCase();
|
||||
var path = parts.slice(1).join(' ');
|
||||
heading.innerHTML = '<span class="bricks-apidoc-method-badge bricks-apidoc-method-' +
|
||||
method.toLowerCase() + '">' + method +
|
||||
'</span><span class="bricks-apidoc-path">' + path + '</span>';
|
||||
} else {
|
||||
heading.textContent = sec.heading;
|
||||
}
|
||||
section_el.appendChild(heading);
|
||||
|
||||
// Section content elements
|
||||
for (var j = 0; j < sec.elements.length; j++) {
|
||||
var el = sec.elements[j].cloneNode(true);
|
||||
this._enhance_element(el);
|
||||
section_el.appendChild(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_enhance_element(el) {
|
||||
// Add classes to tables
|
||||
if (el.tagName === 'TABLE') {
|
||||
el.className = 'bricks-apidoc-table';
|
||||
}
|
||||
|
||||
// Add classes to h3 subsections
|
||||
if (el.tagName === 'H3') {
|
||||
el.className = 'bricks-apidoc-subsection';
|
||||
}
|
||||
|
||||
// Add classes to pre/code blocks
|
||||
if (el.tagName === 'PRE') {
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'bricks-apidoc-code-wrapper';
|
||||
el.parentNode && el.parentNode.insertBefore(wrapper, el);
|
||||
wrapper.appendChild(el);
|
||||
}
|
||||
|
||||
// Add classes to inline tables in parent divs
|
||||
if (el.tagName === 'DIV') {
|
||||
var tables = el.querySelectorAll('table');
|
||||
tables.forEach(function(t) { t.className = 'bricks-apidoc-table'; });
|
||||
}
|
||||
}
|
||||
|
||||
_add_copy_buttons() {
|
||||
var wrappers = this.content_el.querySelectorAll('.bricks-apidoc-code-wrapper');
|
||||
wrappers.forEach(function(wrapper) {
|
||||
var pre = wrapper.querySelector('pre');
|
||||
if (!pre) return;
|
||||
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'bricks-apidoc-copy-btn';
|
||||
btn.textContent = 'Copy';
|
||||
btn.onclick = function() {
|
||||
var code = pre.querySelector('code');
|
||||
var text = code ? code.textContent : pre.textContent;
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
|
||||
});
|
||||
};
|
||||
wrapper.insertBefore(btn, pre);
|
||||
});
|
||||
}
|
||||
|
||||
_scroll_to(section_id) {
|
||||
var el = document.getElementById(section_id);
|
||||
if (!el) return;
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
this._activate_section(section_id);
|
||||
}
|
||||
|
||||
_activate_section(section_id) {
|
||||
if (this.active_section === section_id) return;
|
||||
this.active_section = section_id;
|
||||
|
||||
// Update sidebar active state
|
||||
var items = this.sidebar_el.querySelectorAll('.bricks-apidoc-nav-item');
|
||||
items.forEach(function(item) {
|
||||
item.classList.remove('active');
|
||||
if (item.getAttribute('data-section') === section_id) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_scroll_spy() {
|
||||
var content = this.content_el;
|
||||
var sections = content.querySelectorAll('.bricks-apidoc-section');
|
||||
var scrollTop = content.scrollTop;
|
||||
var found = null;
|
||||
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var sec = sections[i];
|
||||
if (sec.offsetTop - content.offsetTop <= scrollTop + 60) {
|
||||
found = sec.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
this._activate_section(found);
|
||||
}
|
||||
}
|
||||
|
||||
setValue(v) {
|
||||
if (typeof v === 'string') {
|
||||
this._render(v);
|
||||
}
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.md_url;
|
||||
}
|
||||
|
||||
render(params) {
|
||||
if (params && params.md_url) {
|
||||
this.md_url = bricks.absurl(params.md_url);
|
||||
this._load();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bricks.Factory.register('ApiDoc', bricks.ApiDoc);
|
||||
@ -12,7 +12,7 @@ SOURCES=" page_data_loader.js factory.js uitypesdef.js utils.js uitype.js \
|
||||
line.js pie.js bar.js gobang.js period.js iconbarpage.js \
|
||||
keypress.js asr.js webspeech.js countdown.js progressbar.js \
|
||||
qaframe.js svg.js videoplayer.js scatter.js radar.js kline.js \
|
||||
heatmap.js map.js qr.js textfiles.js agent.js "
|
||||
heatmap.js map.js qr.js textfiles.js agent.js api_doc.js "
|
||||
echo ${SOURCES}
|
||||
cat ${SOURCES} > ../dist/bricks.js
|
||||
# uglifyjs --compress --mangle -- ../dist/bricks.js > ../dist/bricks.min.js
|
||||
|
||||
285
bricks/css/api_doc.css
Normal file
285
bricks/css/api_doc.css
Normal file
@ -0,0 +1,285 @@
|
||||
/* ApiDoc Widget Styles */
|
||||
|
||||
.bricks-apidoc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-card, #1a1a2e);
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.bricks-apidoc-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-color, #2a2a4a);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bricks-apidoc-title {
|
||||
font-size: 1.4em;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #fff);
|
||||
}
|
||||
|
||||
.bricks-apidoc-baseurl {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-dim, #888);
|
||||
margin-top: 4px;
|
||||
font-family: 'SFMono-Regular', Consolas, monospace;
|
||||
}
|
||||
|
||||
/* Body: sidebar + content */
|
||||
.bricks-apidoc-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.bricks-apidoc-sidebar {
|
||||
width: 240px;
|
||||
min-width: 200px;
|
||||
border-right: 1px solid var(--border-color, #2a2a4a);
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bricks-apidoc-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
font-size: 0.88em;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.bricks-apidoc-nav-item:hover {
|
||||
background: var(--bg-hover, rgba(255,255,255,0.05));
|
||||
}
|
||||
|
||||
.bricks-apidoc-nav-item.active {
|
||||
background: var(--bg-active, rgba(100,140,255,0.1));
|
||||
border-left-color: var(--accent-blue, #648cff);
|
||||
}
|
||||
|
||||
.bricks-apidoc-nav-path {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text-secondary, #bbb);
|
||||
}
|
||||
|
||||
.bricks-apidoc-nav-item.active .bricks-apidoc-nav-path {
|
||||
color: var(--text-primary, #fff);
|
||||
}
|
||||
|
||||
/* Method badges */
|
||||
.bricks-apidoc-method-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bricks-apidoc-method-get {
|
||||
background: rgba(34,197,94,0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.bricks-apidoc-method-post {
|
||||
background: rgba(59,130,246,0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.bricks-apidoc-method-put {
|
||||
background: rgba(249,115,22,0.15);
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.bricks-apidoc-method-delete {
|
||||
background: rgba(239,68,68,0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.bricks-apidoc-method-patch {
|
||||
background: rgba(168,85,247,0.15);
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
/* Content area */
|
||||
.bricks-apidoc-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 28px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Section */
|
||||
.bricks-apidoc-section {
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 32px;
|
||||
border-bottom: 1px solid var(--border-color, #2a2a4a);
|
||||
}
|
||||
|
||||
.bricks-apidoc-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bricks-apidoc-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
.bricks-apidoc-section-title .bricks-apidoc-method-badge {
|
||||
font-size: 0.8em;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.bricks-apidoc-path {
|
||||
font-family: 'SFMono-Regular', Consolas, monospace;
|
||||
font-size: 0.95em;
|
||||
color: var(--text-primary, #fff);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Subsection headings (h3) */
|
||||
.bricks-apidoc-subsection,
|
||||
.bricks-apidoc-section h3 {
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #ccc);
|
||||
margin: 20px 0 10px 0;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06));
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.bricks-apidoc-table,
|
||||
.bricks-apidoc-section table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.bricks-apidoc-table th,
|
||||
.bricks-apidoc-section table th {
|
||||
background: var(--bg-elevated, rgba(255,255,255,0.04));
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-color, #2a2a4a);
|
||||
color: var(--text-secondary, #aaa);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bricks-apidoc-table td,
|
||||
.bricks-apidoc-section table td {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-color, #2a2a4a);
|
||||
color: var(--text-primary, #ddd);
|
||||
}
|
||||
|
||||
.bricks-apidoc-table tr:hover td,
|
||||
.bricks-apidoc-section table tr:hover td {
|
||||
background: var(--bg-hover, rgba(255,255,255,0.03));
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
.bricks-apidoc-section code {
|
||||
background: var(--bg-elevated, rgba(255,255,255,0.08));
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
font-family: 'SFMono-Regular', Consolas, monospace;
|
||||
color: #e0a8ff;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
.bricks-apidoc-code-wrapper {
|
||||
position: relative;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.bricks-apidoc-code-wrapper pre {
|
||||
background: var(--bg-code, #0d1117) !important;
|
||||
border: 1px solid var(--border-color, #2a2a4a);
|
||||
border-radius: 6px;
|
||||
padding: 14px 16px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bricks-apidoc-code-wrapper pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #c9d1d9;
|
||||
font-family: 'SFMono-Regular', Consolas, monospace;
|
||||
}
|
||||
|
||||
/* Copy button */
|
||||
.bricks-apidoc-copy-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: var(--bg-elevated, rgba(255,255,255,0.1));
|
||||
border: 1px solid var(--border-color, #2a2a4a);
|
||||
color: var(--text-secondary, #aaa);
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bricks-apidoc-code-wrapper:hover .bricks-apidoc-copy-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bricks-apidoc-copy-btn:hover {
|
||||
background: var(--bg-hover, rgba(255,255,255,0.15));
|
||||
color: var(--text-primary, #fff);
|
||||
}
|
||||
|
||||
/* Description paragraphs */
|
||||
.bricks-apidoc-section p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary, #ccc);
|
||||
}
|
||||
|
||||
/* Horizontal rules */
|
||||
.bricks-apidoc-section hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color, #2a2a4a);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
/* Error state */
|
||||
.bricks-apidoc-error {
|
||||
padding: 20px;
|
||||
color: #ef4444;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Responsive: hide sidebar on narrow widths */
|
||||
@media (max-width: 700px) {
|
||||
.bricks-apidoc-sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user