feat: add dark mode, telemetry, and curl to Docker image
- Install `curl` in Dockerfile for healthcheck/tooling support - Add class-based dark mode via Tailwind `@custom-variant` and a pre-paint `<script>` in `app.html` to avoid theme flash on load - Implement theme toggle in layout with system preference detection, `localStorage` persistence, and smooth transitions - Update all UI components with `dark:` variants for full dark mode support across backgrounds, borders, and text colours - Add `sendTelemetry` helper in `api.ts` for fire-and-forget client-side error reporting to `/api/v1/telemetry`
This commit is contained in:
@@ -178,8 +178,8 @@
|
||||
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold text-white">Tool Runner</h1>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">Tool Runner</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Run MCP tools interactively and inspect raw API responses. Useful for spotting strange API
|
||||
results.
|
||||
</p>
|
||||
@@ -189,7 +189,7 @@
|
||||
<!-- Left: selector + form + result -->
|
||||
<div class="lg:col-span-2 space-y-4">
|
||||
<!-- Tool selector -->
|
||||
<div class="rounded-lg bg-gray-900 border border-gray-800 p-4">
|
||||
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4">
|
||||
<h2 class="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">Select Tool</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each TOOLS as tool}
|
||||
@@ -198,11 +198,11 @@
|
||||
class="px-3 py-1.5 rounded text-sm font-medium transition-colors
|
||||
{selectedTool.name === tool.name
|
||||
? 'bg-green-700 text-white'
|
||||
: 'bg-gray-800 text-gray-300 hover:bg-gray-700 border border-gray-700'}"
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-700'}"
|
||||
>
|
||||
{tool.name}
|
||||
{#if tool.premium}
|
||||
<span use:melt={$premTrigger} class="ml-1 text-amber-400 cursor-help">★</span>
|
||||
<span use:melt={$premTrigger} class="ml-1 text-amber-500 dark:text-amber-400 cursor-help">★</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
@@ -210,34 +210,34 @@
|
||||
</div>
|
||||
|
||||
<!-- Parameter form -->
|
||||
<div class="rounded-lg bg-gray-900 border border-gray-800 p-4 space-y-4">
|
||||
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4 space-y-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-sm font-semibold text-white font-mono">{selectedTool.name}</h2>
|
||||
<h2 class="text-sm font-semibold text-gray-900 dark:text-white font-mono">{selectedTool.name}</h2>
|
||||
{#if selectedTool.premium}
|
||||
<span
|
||||
class="text-xs px-1.5 py-0.5 rounded bg-amber-900/60 text-amber-300 border border-amber-800/50"
|
||||
class="text-xs px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/60 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800/50"
|
||||
>
|
||||
Premium
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">{selectedTool.description}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{selectedTool.description}</p>
|
||||
</div>
|
||||
|
||||
{#if selectedTool.params.length > 0}
|
||||
<div class="space-y-3">
|
||||
{#each selectedTool.params as param}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-400 mb-1.5" for={param.key}>
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5" for={param.key}>
|
||||
{param.label}
|
||||
</label>
|
||||
{#if param.type === 'select'}
|
||||
<select
|
||||
id={param.key}
|
||||
bind:value={paramValues[param.key]}
|
||||
class="w-full rounded-md bg-gray-800 border border-gray-700 px-3 py-2 text-sm
|
||||
text-gray-100 focus:outline-none focus:border-green-600 focus:ring-1
|
||||
class="w-full rounded-md bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-3 py-2 text-sm
|
||||
text-gray-900 dark:text-gray-100 focus:outline-none focus:border-green-600 focus:ring-1
|
||||
focus:ring-green-600"
|
||||
>
|
||||
{#each param.options ?? [] as opt}
|
||||
@@ -250,8 +250,8 @@
|
||||
type={param.type === 'number' ? 'number' : param.type === 'date' ? 'date' : 'text'}
|
||||
placeholder={param.placeholder ?? ''}
|
||||
bind:value={paramValues[param.key]}
|
||||
class="w-full rounded-md bg-gray-800 border border-gray-700 px-3 py-2 text-sm
|
||||
text-gray-100 placeholder-gray-600 focus:outline-none focus:border-green-600
|
||||
class="w-full rounded-md bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-3 py-2 text-sm
|
||||
text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-green-600
|
||||
focus:ring-1 focus:ring-green-600"
|
||||
/>
|
||||
{/if}
|
||||
@@ -280,26 +280,26 @@
|
||||
<!-- Result -->
|
||||
{#if result !== null || resultError !== null}
|
||||
<div
|
||||
class="rounded-lg bg-gray-900 border {resultError
|
||||
? 'border-red-800'
|
||||
: 'border-gray-800'}"
|
||||
class="rounded-lg bg-white dark:bg-gray-900 border {resultError
|
||||
? 'border-red-200 dark:border-red-800'
|
||||
: 'border-gray-200 dark:border-gray-800'}"
|
||||
>
|
||||
<div
|
||||
class="px-4 py-2.5 border-b {resultError
|
||||
? 'border-red-800'
|
||||
: 'border-gray-800'} flex items-center gap-2"
|
||||
? 'border-red-200 dark:border-red-800'
|
||||
: 'border-gray-200 dark:border-gray-800'} flex items-center gap-2"
|
||||
>
|
||||
<span class="text-xs font-medium {resultError ? 'text-red-400' : 'text-green-400'}">
|
||||
<span class="text-xs font-medium {resultError ? 'text-red-500 dark:text-red-400' : 'text-green-600 dark:text-green-400'}">
|
||||
{resultError ? 'Error' : 'Result'}
|
||||
</span>
|
||||
{#if result}
|
||||
<span class="text-xs text-gray-600">
|
||||
<span class="text-xs text-gray-500">
|
||||
{result.split('\n').length} lines
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<pre
|
||||
class="px-4 py-4 text-sm font-mono whitespace-pre-wrap text-gray-200 overflow-x-auto
|
||||
class="px-4 py-4 text-sm font-mono whitespace-pre-wrap text-gray-700 dark:text-gray-200 overflow-x-auto
|
||||
max-h-[520px] overflow-y-auto leading-relaxed"
|
||||
>{resultError ?? result}</pre>
|
||||
</div>
|
||||
@@ -307,30 +307,30 @@
|
||||
</div>
|
||||
|
||||
<!-- Right: session history -->
|
||||
<div class="rounded-lg bg-gray-900 border border-gray-800 sticky top-20">
|
||||
<div class="px-4 py-3 border-b border-gray-800">
|
||||
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 sticky top-20">
|
||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-800">
|
||||
<h2 class="text-xs font-medium text-gray-500 uppercase tracking-wider">Session History</h2>
|
||||
</div>
|
||||
{#if history.length === 0}
|
||||
<p class="px-4 py-5 text-sm text-gray-500">No queries yet.</p>
|
||||
{:else}
|
||||
<ul class="divide-y divide-gray-800 max-h-[600px] overflow-y-auto">
|
||||
<ul class="divide-y divide-gray-100 dark:divide-gray-800 max-h-[600px] overflow-y-auto">
|
||||
{#each history as entry}
|
||||
<li>
|
||||
<button
|
||||
onclick={() => loadHistory(entry)}
|
||||
class="w-full text-left px-4 py-3 hover:bg-gray-800/50 transition-colors"
|
||||
class="w-full text-left px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<code class="text-xs {entry.ok ? 'text-green-300' : 'text-red-400'} truncate">
|
||||
<code class="text-xs {entry.ok ? 'text-green-600 dark:text-green-300' : 'text-red-500 dark:text-red-400'} truncate">
|
||||
{entry.tool}
|
||||
</code>
|
||||
<span class="text-xs text-gray-600 shrink-0">{fmtTime(entry.ts)}</span>
|
||||
<span class="text-xs text-gray-500 shrink-0">{fmtTime(entry.ts)}</span>
|
||||
</div>
|
||||
{#each Object.entries(entry.args) as [k, v]}
|
||||
{#if v}
|
||||
<div class="text-xs text-gray-500 truncate mt-0.5">
|
||||
{k}: <span class="text-gray-400">{v}</span>
|
||||
{k}: <span class="text-gray-500 dark:text-gray-400">{v}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -350,7 +350,7 @@
|
||||
{#if $premOpen}
|
||||
<div
|
||||
use:melt={$premContent}
|
||||
class="z-50 rounded bg-gray-800 border border-gray-700 px-2.5 py-1.5 text-xs text-amber-300 shadow-lg"
|
||||
class="z-50 rounded bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 px-2.5 py-1.5 text-xs text-amber-700 dark:text-amber-300 shadow-lg"
|
||||
>
|
||||
Requires a premium TheSportsDB key
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user