Release / build-and-push (push) Successful in 3m31s
templ does not interpolate { expr } inside quoted attribute values — the
braces ship as literal text (AGENTS.md gotcha #1). Swept every templ
source for the pattern and fixed all of it:
Body theme classes: 22 page bodies rendered class="theme-{ user.Theme }"
literally, so every page first-painted the :root default (Tokyo Night)
and flashed when main.js applied the real theme. All now render
class={ "theme-" + user.Theme } server-side.
Attribute interpolations (26 across 8 files):
- api_explorer: initAPIExplorerDoc path/body/response and the Copy
Request/Response buttons (invalid JS, silently dead)
- collections: book-selection :checked/@change/@click ids and four
hx-get search URLs (collection_id={ collection.ID })
- conflicts: Resolve button id
- devices: Kobo sync-url/auth-token element ids (copy lookups only
worked because the id and the lookup were the same literal)
- unlinked_books: search-matches/manual-link clicks and container ids
- queue: status/priority badge classes never matched their CSS
- dashboard: Restore-section tooltip text
- collection_rules/collections: collection icon color
Free text goes through the new templates.JSONArg (json.Marshal) —
templ.JSONString in this templ version returns (string, error) and
cannot compose in attribute expressions; uuids via device.ID.String().
Includes regen-only drift from running templ generate (error-path
normalization) on files with no source change.
115 lines
5.4 KiB
Templ
115 lines
5.4 KiB
Templ
package templates
|
|
|
|
templ APIExplorer(explorer APIExplorerData) {
|
|
if !explorer.IsLoggedIn {
|
|
<div x-data="devices" class="card p-6 mt-8 space-y-5">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
if explorer.Endpoint.Method == "GET" {
|
|
<span class="badge status-completed">{ explorer.Endpoint.Method }</span>
|
|
} else if explorer.Endpoint.Method == "DELETE" {
|
|
<span class="badge status-failed">{ explorer.Endpoint.Method }</span>
|
|
} else if explorer.Endpoint.Method == "POST" {
|
|
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">{ explorer.Endpoint.Method }</span>
|
|
} else {
|
|
<span class="badge status-processing">{ explorer.Endpoint.Method }</span>
|
|
}
|
|
<code class="text-sm" style="color: var(--text-secondary);">{ explorer.Endpoint.Path }</code>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button class="btn btn-primary">Mock Data</button>
|
|
<button class="btn btn-secondary opacity-50 cursor-not-allowed" disabled>Login to Try Real</button>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Request Body (Example)</p>
|
|
<pre class="card p-4 overflow-x-auto"><code class="text-sm" style="color: var(--text-primary);">{ explorer.Endpoint.RequestBody }</code></pre>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Response (Mock)</p>
|
|
<pre class="card p-4 overflow-x-auto"><code class="text-sm" style="color: var(--text-primary);">{ explorer.Endpoint.Response }</code></pre>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button @click={ "copyToClipboard(" + JSONArg(explorer.Endpoint.RequestBody) + ")" } class="btn btn-secondary text-sm">
|
|
@Icon("copy", "h-4 w-4")
|
|
Copy Request
|
|
</button>
|
|
<button @click={ "copyToClipboard(" + JSONArg(explorer.Endpoint.Response) + ")" } class="btn btn-secondary text-sm">
|
|
@Icon("copy", "h-4 w-4")
|
|
Copy Response
|
|
</button>
|
|
</div>
|
|
</div>
|
|
} else {
|
|
<div class="card p-6 mt-8 space-y-5" x-data="apiExplorerDoc" x-init={ "initAPIExplorerDoc(" + JSONArg(explorer.Endpoint.Path) + ", " + JSONArg(explorer.Endpoint.RequestBody) + ", " + JSONArg(explorer.Endpoint.Response) + ")" }>
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
if explorer.Endpoint.Method == "GET" {
|
|
<span class="badge status-completed">{ explorer.Endpoint.Method }</span>
|
|
} else if explorer.Endpoint.Method == "DELETE" {
|
|
<span class="badge status-failed">{ explorer.Endpoint.Method }</span>
|
|
} else if explorer.Endpoint.Method == "POST" {
|
|
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">{ explorer.Endpoint.Method }</span>
|
|
} else {
|
|
<span class="badge status-processing">{ explorer.Endpoint.Method }</span>
|
|
}
|
|
<code class="text-sm" style="color: var(--text-secondary);">{ explorer.Endpoint.Path }</code>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button class="mode-btn btn btn-secondary text-sm" id="mock-btn" @click="showDocMode('mock')">Mock Data</button>
|
|
<button class="mode-btn btn btn-primary text-sm" id="real-btn" @click="showDocMode('real')">Try It Out</button>
|
|
</div>
|
|
<div>
|
|
<label for="http-method" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Method</label>
|
|
<select id="http-method" class="input">
|
|
<option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option>
|
|
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option>
|
|
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option>
|
|
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="request-body" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Request Body</label>
|
|
<textarea id="request-body" placeholder="Edit request body…" class="input min-h-[150px] font-mono">{ explorer.Endpoint.RequestBody }</textarea>
|
|
</div>
|
|
<button id="try-it-out" @click="tryDocEndpoint()" class="btn btn-primary">
|
|
@Icon("play", "h-4 w-4")
|
|
Try It Out
|
|
</button>
|
|
<div class="api-response hidden space-y-2">
|
|
<div class="flex justify-between text-sm">
|
|
<span id="response-status" class="font-semibold" style="color: var(--text-primary);"></span>
|
|
<span id="response-time" style="color: var(--text-secondary);"></span>
|
|
</div>
|
|
<pre class="card p-4 overflow-x-auto"><code id="response-body" class="text-sm" style="color: var(--text-primary);"></code></pre>
|
|
</div>
|
|
<div class="flex gap-2 flex-wrap">
|
|
<button @click="copyDocRequest()" class="btn btn-secondary text-sm">
|
|
@Icon("copy", "h-4 w-4")
|
|
Copy Request
|
|
</button>
|
|
<button @click="copyDocResponse()" class="btn btn-secondary text-sm">
|
|
@Icon("copy", "h-4 w-4")
|
|
Copy Response
|
|
</button>
|
|
<button @click="generateDocCURL()" class="btn btn-secondary text-sm">
|
|
@Icon("external", "h-4 w-4")
|
|
Generate cURL
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
// APIExplorerData holds data for the API explorer component
|
|
type APIExplorerData struct {
|
|
Endpoint EndpointInfo
|
|
IsLoggedIn bool
|
|
}
|
|
|
|
// EndpointInfo holds information about an API endpoint
|
|
type EndpointInfo struct {
|
|
Method string
|
|
Path string
|
|
RequestBody string
|
|
Response string
|
|
Description string
|
|
}
|