style(retro): style API documentation as Win98 windows
Re-skin the API documentation layout for the retro theme to ensure readability and maintain the Windows 98 aesthetic. The default dark revamp tokens were unreadable on the black retro desktop background. Changes include: - Styling the API sidebar as a raised silver window with a classic title bar. - Styling endpoint cards as silver windows with navy title bars. - Excluding API navigation links, shortcut cards, and link pills from default retro link styles to prevent styling conflicts. - Updating API documentation content, including adding a section for resumable uploads.
This commit is contained in:
81
backend/static/api/warpbox.ps1
Normal file
81
backend/static/api/warpbox.ps1
Normal file
@@ -0,0 +1,81 @@
|
||||
#requires -version 5
|
||||
<#
|
||||
.SYNOPSIS
|
||||
warpbox: command line uploader for Warpbox
|
||||
.DESCRIPTION
|
||||
Set the server once, then upload anything:
|
||||
setx WARPBOX_HOST "https://your.warpbox.host"
|
||||
warpbox .\report.pdf
|
||||
|
||||
Install (PowerShell):
|
||||
iwr "$env:WARPBOX_HOST/static/api/warpbox.ps1" -OutFile $HOME\warpbox.ps1
|
||||
# add a function to your $PROFILE: function warpbox { & "$HOME\warpbox.ps1" @args }
|
||||
|
||||
Auth: set the token once so it never lands in your command history.
|
||||
setx WARPBOX_TOKEN "wbx_your_token"
|
||||
Create a token under Account, Access tokens.
|
||||
|
||||
.EXAMPLE
|
||||
.\warpbox.ps1 .\report.pdf
|
||||
.EXAMPLE
|
||||
.\warpbox.ps1 -Password 123 -Expiry 2d .\photo.png .\clip.mp4
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias('p')][string]$Password,
|
||||
[Alias('e')][string]$Expiry,
|
||||
[Alias('n')][int]$MaxDownloads,
|
||||
[Alias('o')][switch]$Obfuscate,
|
||||
[string]$Server = $env:WARPBOX_HOST,
|
||||
[string]$Auth = $env:WARPBOX_TOKEN,
|
||||
[string]$AuthFile,
|
||||
[switch]$Json,
|
||||
[switch]$Help,
|
||||
[Parameter(ValueFromRemainingArguments = $true)][string[]]$Files
|
||||
)
|
||||
|
||||
if ($Help -or -not $Files) {
|
||||
Write-Host 'warpbox: upload files to Warpbox'
|
||||
Write-Host 'USAGE: warpbox.ps1 [-Password pw] [-Expiry 2d] [-MaxDownloads n] [-Obfuscate] [-Json] <file> [file ...]'
|
||||
Write-Host 'SERVER: set WARPBOX_HOST in your environment (setx WARPBOX_HOST "https://your.host")'
|
||||
Write-Host 'AUTH: set WARPBOX_TOKEN in your environment (setx WARPBOX_TOKEN "wbx_...")'
|
||||
if (-not $Files -and -not $Help) { exit 2 } else { exit 0 }
|
||||
}
|
||||
|
||||
if (-not $Server) {
|
||||
Write-Error 'warpbox: no server set. Use -Server <url> or set WARPBOX_HOST'
|
||||
exit 2
|
||||
}
|
||||
if ($AuthFile) { $Auth = (Get-Content -Raw $AuthFile).Trim() }
|
||||
|
||||
function ConvertTo-Minutes($v) {
|
||||
if ($v -match '^(\d+)([mhdw]?)$') {
|
||||
$n = [int]$Matches[1]
|
||||
switch ($Matches[2]) {
|
||||
'h' { return $n * 60 }
|
||||
'd' { return $n * 1440 }
|
||||
'w' { return $n * 10080 }
|
||||
default { return $n }
|
||||
}
|
||||
}
|
||||
return $v
|
||||
}
|
||||
|
||||
# Expand wildcards (PowerShell does not expand them in arguments).
|
||||
$expanded = @()
|
||||
foreach ($f in $Files) {
|
||||
$hits = Get-ChildItem -Path $f -File -ErrorAction SilentlyContinue
|
||||
if ($hits) { $expanded += $hits.FullName } else { $expanded += $f }
|
||||
}
|
||||
|
||||
$curlArgs = @('-fS')
|
||||
foreach ($f in $expanded) { $curlArgs += @('-F', "file=@$f") }
|
||||
if ($Password) { $curlArgs += @('-F', "password=$Password") }
|
||||
if ($Expiry) { $curlArgs += @('-F', "expires_minutes=$(ConvertTo-Minutes $Expiry)") }
|
||||
if ($MaxDownloads) { $curlArgs += @('-F', "max_downloads=$MaxDownloads") }
|
||||
if ($Obfuscate) { $curlArgs += @('-F', 'obfuscate_metadata=on') }
|
||||
if ($Auth) { $curlArgs += @('-H', "Authorization: Bearer $Auth") }
|
||||
if ($Json) { $curlArgs += @('-H', 'Accept: application/json') }
|
||||
$curlArgs += "$($Server.TrimEnd('/'))/api/v1/upload"
|
||||
|
||||
& curl.exe @curlArgs
|
||||
120
backend/static/api/warpbox.sh
Normal file
120
backend/static/api/warpbox.sh
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# warpbox: command line uploader for Warpbox
|
||||
#
|
||||
# Set the server once, then upload anything:
|
||||
# export WARPBOX_HOST=https://your.warpbox.host
|
||||
# warpbox ./report.pdf
|
||||
#
|
||||
# Install:
|
||||
# curl -fsSL "$WARPBOX_HOST/static/api/warpbox.sh" -o ~/.local/bin/warpbox
|
||||
# chmod +x ~/.local/bin/warpbox
|
||||
# # make sure ~/.local/bin is on your PATH
|
||||
#
|
||||
set -eo pipefail
|
||||
|
||||
WARPBOX_HOST="${WARPBOX_HOST:-}"
|
||||
AUTH="${WARPBOX_TOKEN:-}"
|
||||
PASSWORD=""
|
||||
EXPIRY=""
|
||||
MAX_DOWNLOADS=""
|
||||
OBFUSCATE=""
|
||||
AS_JSON=0
|
||||
FILES=()
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
warpbox: upload files to Warpbox from the terminal
|
||||
|
||||
USAGE:
|
||||
warpbox [options] <file> [file ...]
|
||||
|
||||
OPTIONS:
|
||||
-p, --password <pw> Require a password to view/download the box
|
||||
-e, --expiry <dur> Lifetime before expiry: 30m, 6h, 2d, 1w (or bare minutes)
|
||||
-n, --max-downloads <n> Expire after N downloads
|
||||
-o, --obfuscate Hide file names/counts until unlocked (needs --password)
|
||||
--host <url> Warpbox server to upload to (or set WARPBOX_HOST)
|
||||
--auth <token> API token (prefer the WARPBOX_TOKEN env var, see AUTH)
|
||||
--auth-file <path> Read the API token from a file (safer than --auth)
|
||||
--json Print the full JSON response instead of just the URL
|
||||
-h, --help Show this help
|
||||
|
||||
AUTH:
|
||||
Uploads are anonymous unless a token is supplied. The most secure option is the
|
||||
WARPBOX_TOKEN environment variable, so the token never lands in your shell
|
||||
history or the process list:
|
||||
|
||||
export WARPBOX_TOKEN=wbx_your_token
|
||||
warpbox ./photo.png
|
||||
|
||||
Create a token under Account, Access tokens. Avoid --auth on shared machines.
|
||||
|
||||
EXAMPLES:
|
||||
warpbox ./report.pdf
|
||||
warpbox --password 123 --expiry 2d ./first_file.zip ./whatever.png ./all_*_photos.jpg
|
||||
warpbox --max-downloads 5 --json ./build.zip
|
||||
EOF
|
||||
}
|
||||
|
||||
expiry_to_minutes() {
|
||||
local v="$1" num unit
|
||||
num="${v%%[mhdw]*}"
|
||||
unit="${v##*[0-9]}"
|
||||
case "$unit" in
|
||||
h) echo $(( num * 60 )) ;;
|
||||
d) echo $(( num * 1440 )) ;;
|
||||
w) echo $(( num * 10080 )) ;;
|
||||
m|"") echo "$num" ;;
|
||||
*) echo "$num" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-p|--password) PASSWORD="$2"; shift 2 ;;
|
||||
-e|--expiry) EXPIRY="$2"; shift 2 ;;
|
||||
-n|--max-downloads) MAX_DOWNLOADS="$2"; shift 2 ;;
|
||||
-o|--obfuscate) OBFUSCATE="on"; shift ;;
|
||||
--host) WARPBOX_HOST="$2"; shift 2 ;;
|
||||
--auth) AUTH="$2"; shift 2 ;;
|
||||
--auth-file) AUTH="$(cat "$2")"; shift 2 ;;
|
||||
--json) AS_JSON=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--) shift; while [ $# -gt 0 ]; do FILES+=("$1"); shift; done ;;
|
||||
-*) echo "warpbox: unknown option $1" >&2; exit 2 ;;
|
||||
*) FILES+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$WARPBOX_HOST" ]; then
|
||||
echo "warpbox: no server set. Use --host <url> or export WARPBOX_HOST=<url>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ${#FILES[@]} -eq 0 ]; then
|
||||
echo "warpbox: no files given" >&2
|
||||
echo >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
CURL_ARGS=()
|
||||
for f in "${FILES[@]}"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "warpbox: not a file: $f" >&2
|
||||
exit 2
|
||||
fi
|
||||
CURL_ARGS+=(-F "file=@${f}")
|
||||
done
|
||||
|
||||
[ -n "$PASSWORD" ] && CURL_ARGS+=(-F "password=${PASSWORD}")
|
||||
[ -n "$EXPIRY" ] && CURL_ARGS+=(-F "expires_minutes=$(expiry_to_minutes "$EXPIRY")")
|
||||
[ -n "$MAX_DOWNLOADS" ] && CURL_ARGS+=(-F "max_downloads=${MAX_DOWNLOADS}")
|
||||
[ -n "$OBFUSCATE" ] && CURL_ARGS+=(-F "obfuscate_metadata=on")
|
||||
|
||||
HEADERS=()
|
||||
[ -n "$AUTH" ] && HEADERS+=(-H "Authorization: Bearer ${AUTH}")
|
||||
[ "$AS_JSON" -eq 1 ] && HEADERS+=(-H "Accept: application/json")
|
||||
|
||||
exec curl -fS "${HEADERS[@]}" "${CURL_ARGS[@]}" "${WARPBOX_HOST%/}/api/v1/upload"
|
||||
@@ -152,16 +152,16 @@
|
||||
|
||||
/* Links: classic blue, underlined, purple when visited. Sidebar links and tabs
|
||||
are styled as their own Win98 controls below, so they're excluded here. */
|
||||
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link) {
|
||||
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):not(.api-nav-link):not(.shortcut-card):not(.link-pill) {
|
||||
color: #0000ee;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):visited {
|
||||
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):not(.api-nav-link):not(.shortcut-card):not(.link-pill):visited {
|
||||
color: #551a8b;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):hover {
|
||||
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):not(.api-nav-link):not(.shortcut-card):not(.link-pill):hover {
|
||||
color: #ee0000;
|
||||
}
|
||||
|
||||
@@ -741,3 +741,219 @@
|
||||
:root[data-theme="retro"] .file-main small {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* API documentation: sidebar + panels as Win98 windows */
|
||||
/* The new .api-docs layout uses dark revamp tokens by default, which are */
|
||||
/* unreadable on the black retro desktop. Re-skin it as Win98 chrome: a */
|
||||
/* raised silver sidebar window, plain light section intros on the desktop, */
|
||||
/* and each card a silver window with a navy title bar from its heading. */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Sidebar = raised silver window with a real title bar from its <h1>. */
|
||||
:root[data-theme="retro"] .api-sidebar {
|
||||
background: linear-gradient(to bottom, #ffffff, 4%, #c0c0c0 8%);
|
||||
background-color: #c0c0c0;
|
||||
border: 1px solid #000000;
|
||||
box-shadow: var(--shadow);
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .api-sidebar > .kicker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .api-sidebar-title {
|
||||
margin: -0.5rem -0.5rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .api-nav {
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
/* Nav entries are flat silver list items; the active one is a navy bar. */
|
||||
:root[data-theme="retro"] .api-nav-link {
|
||||
color: #000000;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .api-nav-link:hover {
|
||||
background: #d4d0c8;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .api-nav-link.is-active {
|
||||
background: linear-gradient(to right, #000078, 80%, #0f80cd);
|
||||
color: #ffffff;
|
||||
border-color: #000000;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .api-sidebar-meta {
|
||||
border-top: 1px solid #808080;
|
||||
box-shadow: 0 -1px 0 #ffffff;
|
||||
padding-top: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Section intros sit on the black desktop: gold kicker, white title, light
|
||||
subtitle, and readable inline code (not the default black-on-black). */
|
||||
:root[data-theme="retro"] .panel-head .kicker {
|
||||
color: #ffd966;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .panel-head h2,
|
||||
:root[data-theme="retro"] .section-label {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .panel-head .lead {
|
||||
color: #cfd8ff;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .panel-head .lead code {
|
||||
color: #ffffff;
|
||||
background: #000078;
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
|
||||
/* Each card heading becomes a Win98 title bar with a fake close button.
|
||||
Headings bleed to the window edges; only the first hugs the top edge so a
|
||||
multi-step card (e.g. ShareX) reads as stacked group bars, not overlaps. */
|
||||
:root[data-theme="retro"] .api-content .card > .card-content > h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin: 1.5rem -1.5rem 1rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: linear-gradient(to right, #000078, 80%, #0f80cd);
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .api-content .card > .card-content > h3:first-child {
|
||||
margin-top: -1.5rem;
|
||||
}
|
||||
|
||||
/* The upload endpoint card leads with a method + path row; make that the bar. */
|
||||
:root[data-theme="retro"] .api-content .endpoint-head {
|
||||
margin: -1.5rem -1.5rem 1rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
background: linear-gradient(to right, #000078, 80%, #0f80cd);
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .endpoint-head .endpoint-path {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .api-content .card > .card-content > h3::after,
|
||||
:root[data-theme="retro"] .api-content .endpoint-head::after {
|
||||
content: "\2715";
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 1.15rem;
|
||||
height: 1rem;
|
||||
margin-left: auto;
|
||||
background: #c0c0c0;
|
||||
color: #000000;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf;
|
||||
}
|
||||
|
||||
/* Body text inside windows reads black, not muted purple. */
|
||||
:root[data-theme="retro"] .api-content .card p,
|
||||
:root[data-theme="retro"] .api-content .card h4,
|
||||
:root[data-theme="retro"] .api-content .field-grid span,
|
||||
:root[data-theme="retro"] .endpoint-list div em,
|
||||
:root[data-theme="retro"] .faq-item summary,
|
||||
:root[data-theme="retro"] .faq-item p {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Sub-labels (Request fields, Example, ...) become small black headers. */
|
||||
:root[data-theme="retro"] .api-content .card h4 {
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* Endpoint rows are sunken white fields. */
|
||||
:root[data-theme="retro"] .endpoint-list div {
|
||||
background: #ffffff;
|
||||
border: 1px solid #000000;
|
||||
box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff;
|
||||
}
|
||||
|
||||
/* Home shortcut tiles and quick links: silver windows / sunken white fields. */
|
||||
:root[data-theme="retro"] .shortcut-card {
|
||||
background: linear-gradient(to bottom, #ffffff, 6%, #c0c0c0 10%);
|
||||
background-color: #c0c0c0;
|
||||
border: 1px solid #000000;
|
||||
box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .shortcut-card:hover {
|
||||
transform: none;
|
||||
background-color: #d4d0c8;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .shortcut-eyebrow {
|
||||
color: #000078;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .shortcut-title,
|
||||
:root[data-theme="retro"] .shortcut-sub {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .link-pill {
|
||||
background: #ffffff;
|
||||
border: 1px solid #000000;
|
||||
box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .link-pill span {
|
||||
background: #000078;
|
||||
color: #ffffff;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
/* CLI download cards = silver windows. */
|
||||
:root[data-theme="retro"] .download-card {
|
||||
background: linear-gradient(to bottom, #ffffff, 4%, #c0c0c0 8%);
|
||||
background-color: #c0c0c0;
|
||||
border: 1px solid #000000;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .download-card .download-os,
|
||||
:root[data-theme="retro"] .download-card p {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* FAQ entries are silver windows; the +/- marker stays. */
|
||||
:root[data-theme="retro"] .faq-item {
|
||||
background: linear-gradient(to bottom, #ffffff, 4%, #c0c0c0 8%);
|
||||
background-color: #c0c0c0;
|
||||
border: 1px solid #000000;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
:root[data-theme="retro"] .faq-item summary::after {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* Copy buttons: stay visible (retro already paints them as silver buttons). */
|
||||
:root[data-theme="retro"] .code-block .copy-btn {
|
||||
background: #c0c0c0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,408 @@
|
||||
padding: 2rem 0 3rem;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
API documentation — sidebar layout
|
||||
============================================================ */
|
||||
|
||||
.api-docs {
|
||||
width: min(74rem, calc(100% - 2rem));
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0 3rem;
|
||||
display: grid;
|
||||
grid-template-columns: 13.5rem minmax(0, 1fr);
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.api-sidebar {
|
||||
position: sticky;
|
||||
top: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.api-sidebar-title {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.api-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
border-left: 1px solid var(--border);
|
||||
padding-left: 0.3rem;
|
||||
}
|
||||
|
||||
.api-nav-link {
|
||||
display: block;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border-radius: calc(var(--radius) - 0.3rem);
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
line-height: 1.2;
|
||||
transition: background 0.12s ease, color 0.12s ease;
|
||||
}
|
||||
|
||||
.api-nav-link:hover {
|
||||
background: var(--muted);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.api-nav-link.is-active {
|
||||
background: color-mix(in srgb, var(--primary) 16%, transparent);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.api-sidebar-meta {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.api-sidebar-meta a {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* --- Panels: only one visible at a time --- */
|
||||
.api-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.doc-panel {
|
||||
display: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.doc-panel.is-active {
|
||||
display: block;
|
||||
animation: doc-fade 0.18s ease;
|
||||
}
|
||||
|
||||
@keyframes doc-fade {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
max-width: 46rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.panel-head h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.panel-head .lead {
|
||||
margin: 0.6rem 0 0;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.api-content .card + .card,
|
||||
.api-content .quickstart {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.api-content h3 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.api-content h4 {
|
||||
margin: 1.4rem 0 0;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.api-content .card p {
|
||||
margin: 0.65rem 0 0;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.api-content code {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.api-content .field-grid p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
margin: 1.75rem 0 0.75rem !important;
|
||||
font-size: 0.8rem !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* --- Home shortcuts --- */
|
||||
.shortcut-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.shortcut-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: color-mix(in srgb, var(--card) 94%, transparent);
|
||||
text-decoration: none;
|
||||
transition: border-color 0.12s ease, transform 0.12s ease;
|
||||
}
|
||||
|
||||
.shortcut-card:hover {
|
||||
border-color: var(--ring);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.shortcut-eyebrow {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.shortcut-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.shortcut-sub {
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.link-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.link-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: calc(var(--radius) - 0.2rem);
|
||||
background: var(--card);
|
||||
color: var(--foreground);
|
||||
font-size: 0.88rem;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.12s ease;
|
||||
}
|
||||
|
||||
.link-pill:hover {
|
||||
border-color: var(--ring);
|
||||
}
|
||||
|
||||
.link-pill span {
|
||||
flex: none;
|
||||
min-width: 2.6rem;
|
||||
text-align: center;
|
||||
padding: 0.15rem 0.35rem;
|
||||
border-radius: 0.3rem;
|
||||
background: var(--muted);
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* --- Code blocks with copy button --- */
|
||||
.code-block {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-block .copy-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.4rem;
|
||||
background: color-mix(in srgb, var(--card) 80%, transparent);
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s ease, color 0.12s ease, border-color 0.12s ease;
|
||||
}
|
||||
|
||||
.code-block:hover .copy-btn,
|
||||
.code-block .copy-btn:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.code-block .copy-btn:hover {
|
||||
color: var(--foreground);
|
||||
border-color: var(--ring);
|
||||
}
|
||||
|
||||
/* --- Endpoint blocks --- */
|
||||
.endpoint-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.endpoint-path {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.method {
|
||||
flex: none;
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.35rem;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.method-get { background: #2563eb; }
|
||||
.method-post { background: #16a34a; }
|
||||
.method-put { background: #d97706; }
|
||||
|
||||
.endpoint-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0 0;
|
||||
}
|
||||
|
||||
.endpoint-list div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.55rem 0.7rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: calc(var(--radius) - 0.3rem);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.endpoint-list div code {
|
||||
font-size: 0.82rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.endpoint-list div em {
|
||||
margin-left: auto;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.8rem;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* --- CLI download cards --- */
|
||||
.download-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.download-card {
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: color-mix(in srgb, var(--card) 94%, transparent);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.download-card .download-os {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 650;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.download-card p {
|
||||
margin: 0;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.download-card .button {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* --- FAQ --- */
|
||||
.faq-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.faq-item {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: calc(var(--radius) - 0.2rem);
|
||||
background: color-mix(in srgb, var(--card) 94%, transparent);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.faq-item summary {
|
||||
padding: 0.9rem 0;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
list-style: none;
|
||||
position: relative;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.faq-item summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.faq-item summary::after {
|
||||
content: "+";
|
||||
position: absolute;
|
||||
right: 0.1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--muted-foreground);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.faq-item[open] summary::after {
|
||||
content: "\2212";
|
||||
}
|
||||
|
||||
.faq-item p {
|
||||
margin: 0 0 0.95rem;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.docs-header {
|
||||
max-width: 44rem;
|
||||
}
|
||||
@@ -63,42 +465,19 @@
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.endpoint-list,
|
||||
.field-grid {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
margin: 1rem 0 0;
|
||||
}
|
||||
|
||||
.endpoint-list div,
|
||||
.field-grid {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.endpoint-list div {
|
||||
display: grid;
|
||||
grid-template-columns: 7rem minmax(0, 1fr);
|
||||
gap: 0.75rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.endpoint-list dt,
|
||||
.endpoint-list dd {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.endpoint-list dt,
|
||||
.field-grid span {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.endpoint-list dd code {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.docs-steps {
|
||||
margin: 0.85rem 0 0;
|
||||
padding-left: 1.1rem;
|
||||
|
||||
@@ -57,6 +57,44 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.api-docs {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.api-sidebar {
|
||||
position: static;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.api-sidebar-title {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.api-nav {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.api-nav-link {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.api-sidebar-meta {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.endpoint-list div em {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
position: static;
|
||||
width: 100%;
|
||||
|
||||
94
backend/static/js/48-api-docs.js
Normal file
94
backend/static/js/48-api-docs.js
Normal file
@@ -0,0 +1,94 @@
|
||||
(function () {
|
||||
const root = document.querySelector("[data-api-docs]");
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const panels = Array.from(root.querySelectorAll("[data-doc-panel]"));
|
||||
const navLinks = Array.from(root.querySelectorAll("[data-doc-link]"));
|
||||
const DEFAULT = "home";
|
||||
|
||||
function activate(name, focus) {
|
||||
let matched = false;
|
||||
panels.forEach((panel) => {
|
||||
const on = panel.dataset.docPanel === name;
|
||||
panel.classList.toggle("is-active", on);
|
||||
if (on) {
|
||||
matched = true;
|
||||
}
|
||||
});
|
||||
if (!matched) {
|
||||
return false;
|
||||
}
|
||||
root.querySelectorAll(".api-nav-link").forEach((link) => {
|
||||
link.classList.toggle(
|
||||
"is-active",
|
||||
link.getAttribute("href") === "#" + name
|
||||
);
|
||||
});
|
||||
if (focus) {
|
||||
const panel = root.querySelector('[data-doc-panel="' + name + '"]');
|
||||
if (panel) {
|
||||
panel.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve the current hash to a panel. The hash can point at a panel id
|
||||
// (e.g. #endpoints) or at any element inside a panel (e.g. #ep-upload),
|
||||
// letting FAQ answers deep-link straight into the reference.
|
||||
function resolveHash(focus) {
|
||||
const id = (location.hash || "").slice(1);
|
||||
if (!id) {
|
||||
activate(DEFAULT, focus);
|
||||
return;
|
||||
}
|
||||
const target = document.getElementById(id);
|
||||
if (!target) {
|
||||
activate(DEFAULT, focus);
|
||||
return;
|
||||
}
|
||||
const panel = target.closest("[data-doc-panel]");
|
||||
const name = panel ? panel.dataset.docPanel : DEFAULT;
|
||||
activate(name, focus && target === panel);
|
||||
if (panel && target !== panel) {
|
||||
// Scroll the deep-linked element into view once its panel is visible.
|
||||
window.requestAnimationFrame(() => {
|
||||
target.scrollIntoView({ block: "start", behavior: "smooth" });
|
||||
});
|
||||
} else {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", () => resolveHash(true));
|
||||
|
||||
navLinks.forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
// hashchange handles activation; this keeps top-level nav clicks snappy.
|
||||
if (link.getAttribute("href") === location.hash) {
|
||||
resolveHash(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add a copy button to every code block.
|
||||
root.querySelectorAll(".code-block").forEach((block) => {
|
||||
const pre = block.querySelector("pre");
|
||||
if (!pre) {
|
||||
return;
|
||||
}
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "copy-btn";
|
||||
button.textContent = "Copy";
|
||||
button.setAttribute("aria-label", "Copy code");
|
||||
button.addEventListener("click", () => {
|
||||
window.Warpbox.copyText(pre.innerText.trim(), button, "Copied");
|
||||
});
|
||||
block.appendChild(button);
|
||||
});
|
||||
|
||||
resolveHash(false);
|
||||
})();
|
||||
Reference in New Issue
Block a user