tracks lexicons and how many times they appeared on the jetstream

feat(client): add dark mode

ptr.pet b7d77d22 e8cda421

verified
+9 -9
client/src/app.html
···
<!doctype html>
<html lang="en">
-
<head>
-
<meta charset="utf-8" />
-
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
-
%sveltekit.head%
-
</head>
-
<body data-sveltekit-preload-data="hover">
-
<div style="display: contents">%sveltekit.body%</div>
-
</body>
</html>
···
<!doctype html>
<html lang="en">
+
<head>
+
<meta charset="utf-8" />
+
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
+
%sveltekit.head%
+
</head>
+
<body class="bg-white dark:bg-gray-900" data-sveltekit-preload-data="hover">
+
<div style="display: contents">%sveltekit.body%</div>
+
</body>
</html>
+2 -2
client/src/lib/components/BskyToggle.svelte
···
<!-- svelte-ignore a11y_no_static_element_interactions -->
<button
onclick={onBskyToggle}
-
class="wsbadge !mt-0 !font-normal bg-blue-100 hover:bg-blue-200 border-blue-300"
>
<input checked={dontShowBsky} type="checkbox" />
-
<span class="ml-0.5"> hide app.bsky.* </span>
</button>
···
<!-- svelte-ignore a11y_no_static_element_interactions -->
<button
onclick={onBskyToggle}
+
class="wsbadge !mt-0 !font-normal bg-blue-100 dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800 border-blue-300 dark:border-blue-700"
>
<input checked={dontShowBsky} type="checkbox" />
+
<span class="ml-0.5 text-black dark:text-gray-200"> hide app.bsky.* </span>
</button>
+8 -5
client/src/lib/components/EventCard.svelte
···
</script>
<div
-
class="group flex flex-col gap-2 p-1.5 md:p-3 min-h-64 bg-white border border-gray-200 rounded-lg hover:shadow-lg md:hover:-translate-y-1 transition-all duration-200 transform"
class:has-activity={isAnimating}
style="--border-thickness: {borderThickness}px"
>
<div class="flex items-start gap-2">
<div
-
class="text-sm font-bold text-blue-600 bg-blue-100 px-3 py-1 rounded-full"
>
#{index + 1}
</div>
<div
title={event.nsid}
-
class="font-mono text-sm text-gray-700 mt-0.5 leading-relaxed rounded-full text-nowrap text-ellipsis overflow-hidden group-hover:overflow-visible group-hover:bg-gray-50 border-gray-100 group-hover:border transition-all px-1"
>
{event.nsid}
</div>
···
</div>
</div>
-
<style>
.has-activity {
position: relative;
transition: all 0.2s ease-out;
}
.has-activity::before {
content: "";
position: absolute;
top: calc(-1 * var(--border-thickness));
left: calc(-1 * var(--border-thickness));
right: calc(-1 * var(--border-thickness));
bottom: calc(-1 * var(--border-thickness));
-
border: var(--border-thickness) solid rgba(59, 130, 246, 0.8);
border-radius: calc(0.5rem + var(--border-thickness));
pointer-events: none;
transition: all 0.3s ease-out;
···
</script>
<div
+
class="group flex flex-col gap-2 p-1.5 md:p-3 min-h-64 bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-gray-950 rounded-lg hover:shadow-lg md:hover:-translate-y-1 transition-all duration-200 transform"
class:has-activity={isAnimating}
style="--border-thickness: {borderThickness}px"
>
<div class="flex items-start gap-2">
<div
+
class="text-sm font-bold text-blue-600 bg-blue-100 dark:bg-indigo-950 px-3 py-1 rounded-full"
>
#{index + 1}
</div>
<div
title={event.nsid}
+
class="font-mono text-sm text-gray-700 dark:text-gray-300 mt-0.5 leading-relaxed rounded-full text-nowrap text-ellipsis overflow-hidden group-hover:overflow-visible group-hover:bg-gray-50 dark:group-hover:bg-gray-700 border-gray-100 dark:border-gray-900 group-hover:border transition-all px-1"
>
{event.nsid}
</div>
···
</div>
</div>
+
<style lang="postcss">
.has-activity {
position: relative;
transition: all 0.2s ease-out;
}
.has-activity::before {
+
@reference "../../app.css";
+
@apply border-blue-500 dark:border-blue-800;
content: "";
position: absolute;
top: calc(-1 * var(--border-thickness));
left: calc(-1 * var(--border-thickness));
right: calc(-1 * var(--border-thickness));
bottom: calc(-1 * var(--border-thickness));
+
border-width: var(--border-thickness);
+
border-style: solid;
border-radius: calc(0.5rem + var(--border-thickness));
pointer-events: none;
transition: all 0.3s ease-out;
+5 -3
client/src/lib/components/FilterControls.svelte
···
</script>
<div
-
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-blue-100 hover:bg-blue-200 border-blue-300"
>
-
<label for="filter-regex" class="text-blue-800 mr-1"> filter: </label>
<input
id="filter-regex"
value={filterRegex}
oninput={(e) => onFilterChange((e.target as HTMLInputElement).value)}
type="text"
placeholder="regex..."
-
class="bg-blue-50 text-blue-900 placeholder-blue-400 border border-blue-200 rounded-full px-1 outline-none focus:bg-white focus:border-blue-400 min-w-0 w-24"
/>
</div>
···
</script>
<div
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-blue-100 dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800 border-blue-300 dark:border-blue-700"
>
+
<label for="filter-regex" class="text-blue-800 dark:text-gray-200 mr-1">
+
filter:
+
</label>
<input
id="filter-regex"
value={filterRegex}
oninput={(e) => onFilterChange((e.target as HTMLInputElement).value)}
type="text"
placeholder="regex..."
+
class="bg-blue-50 dark:bg-blue-950 text-blue-900 dark:text-gray-400 placeholder-blue-400 dark:placeholder-blue-700 border border-blue-200 dark:border-blue-700 rounded-full px-1 outline-none focus:border-blue-400 min-w-0 w-24"
/>
</div>
+6 -4
client/src/lib/components/RefreshControl.svelte
···
</script>
<div
-
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-lime-100 hover:bg-lime-200 border-lime-300"
>
-
<label for="refresh-rate" class="text-lime-800 mr-1">refresh:</label>
<input
id="refresh-rate"
value={refreshRate}
···
pattern="[0-9]*"
min="0"
placeholder="real-time"
-
class="bg-green-50 text-lime-900 placeholder-lime-600 border border-lime-200 rounded-full px-1 outline-none focus:bg-white focus:border-lime-400 min-w-0 w-20"
/>
-
<span class="text-lime-700">s</span>
</div>
···
</script>
<div
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-lime-100 dark:bg-lime-900 dark:hover:bg-lime-800 hover:bg-lime-200 border-lime-300 dark:border-lime-700"
>
+
<label for="refresh-rate" class="text-lime-800 dark:text-lime-200 mr-1"
+
>refresh:</label
+
>
<input
id="refresh-rate"
value={refreshRate}
···
pattern="[0-9]*"
min="0"
placeholder="real-time"
+
class="bg-green-50 dark:bg-green-900 text-lime-900 dark:text-lime-200 placeholder-lime-600 dark:placeholder-lime-400 border border-lime-200 dark:border-lime-700 rounded-full px-1 outline-none focus:border-lime-400 min-w-0 w-20"
/>
+
<span class="text-lime-800 dark:text-lime-200">s</span>
</div>
+5 -3
client/src/lib/components/ShowControls.svelte
···
</script>
<div
-
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-pink-100 hover:bg-pink-200 border-pink-300"
>
-
<label for="show" class="text-pink-800 mr-1"> show since: </label>
<select
id="show"
value={show}
onchange={(e) =>
onShowChange((e.target as HTMLSelectElement).value as ShowOption)}
-
class="bg-pink-50 text-pink-900 border border-pink-200 rounded-full px-1 outline-none focus:bg-white focus:border-pink-400 min-w-0"
>
{#each showOptions as option}
<option value={option}>{option}</option>
···
</script>
<div
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-pink-100 dark:bg-pink-800 hover:bg-pink-200 dark:hover:bg-pink-700 border-pink-300 dark:border-pink-700"
>
+
<label for="show" class="text-pink-800 dark:text-pink-100 mr-1">
+
show since:
+
</label>
<select
id="show"
value={show}
onchange={(e) =>
onShowChange((e.target as HTMLSelectElement).value as ShowOption)}
+
class="bg-pink-50 dark:bg-pink-900 text-pink-900 dark:text-pink-100 border border-pink-200 dark:border-pink-700 rounded-full px-1 outline-none focus:border-pink-400 min-w-0"
>
{#each showOptions as option}
<option value={option}>{option}</option>
+5 -3
client/src/lib/components/SortControls.svelte
···
</script>
<div
-
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-purple-100 hover:bg-purple-200 border-purple-300"
>
-
<label for="sort-by" class="text-purple-800 mr-1"> sort by: </label>
<select
id="sort-by"
value={sortBy}
onchange={(e) =>
onSortChange((e.target as HTMLSelectElement).value as SortOption)}
-
class="bg-purple-50 text-purple-900 border border-purple-200 rounded-full px-1 outline-none focus:bg-white focus:border-purple-400 min-w-0"
>
{#each sortOptions as option}
<option value={option.value}>{option.label}</option>
···
</script>
<div
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-300 dark:border-purple-700"
>
+
<label for="sort-by" class="text-purple-800 dark:text-purple-300 mr-1">
+
sort by:
+
</label>
<select
id="sort-by"
value={sortBy}
onchange={(e) =>
onSortChange((e.target as HTMLSelectElement).value as SortOption)}
+
class="bg-purple-50 dark:bg-purple-900 text-purple-900 dark:text-purple-300 border border-purple-200 dark:border-purple-700 rounded-full px-1 outline-none focus:border-purple-400 min-w-0"
>
{#each sortOptions as option}
<option value={option.value}>{option.label}</option>
+16 -16
client/src/lib/components/StatsCard.svelte
···
const colorClasses = {
green: {
-
bg: "from-green-50 to-green-100",
-
border: "border-green-200",
-
titleText: "text-green-700",
-
valueText: "text-green-900",
},
red: {
-
bg: "from-red-50 to-red-100",
-
border: "border-red-200",
-
titleText: "text-red-700",
-
valueText: "text-red-900",
},
turqoise: {
-
bg: "from-teal-50 to-teal-100",
-
border: "border-teal-200",
-
titleText: "text-teal-700",
-
valueText: "text-teal-900",
},
orange: {
-
bg: "from-orange-50 to-orange-100",
-
border: "border-orange-200",
-
titleText: "text-orange-700",
-
valueText: "text-orange-900",
},
};
···
const colorClasses = {
green: {
+
bg: "from-green-50 to-green-100 dark:from-green-900 dark:to-green-800",
+
border: "border-green-200 dark:border-green-800",
+
titleText: "text-green-700 dark:text-green-400",
+
valueText: "text-green-900 dark:text-green-200",
},
red: {
+
bg: "from-red-50 to-red-100 dark:from-red-900 dark:to-red-800",
+
border: "border-red-200 dark:border-red-800",
+
titleText: "text-red-700 dark:text-red-400",
+
valueText: "text-red-900 dark:text-red-200",
},
turqoise: {
+
bg: "from-teal-50 to-teal-100 dark:from-teal-900 dark:to-teal-800",
+
border: "border-teal-200 dark:border-teal-800",
+
titleText: "text-teal-700 dark:text-teal-400",
+
valueText: "text-teal-900 dark:text-teal-200",
},
orange: {
+
bg: "from-orange-50 to-orange-100 dark:from-orange-900 dark:to-orange-800",
+
border: "border-orange-200 dark:border-orange-800",
+
titleText: "text-orange-700 dark:text-orange-400",
+
valueText: "text-orange-900 dark:text-orange-200",
},
};
+9 -5
client/src/lib/components/StatusBadge.svelte
···
const statusConfig = {
connected: {
text: "stream live",
-
classes: "bg-green-100 text-green-800 border-green-200",
},
connecting: {
text: "stream connecting",
-
classes: "bg-yellow-100 text-yellow-800 border-yellow-200",
},
error: {
text: "stream errored",
-
classes: "bg-red-100 text-red-800 border-red-200",
},
disconnected: {
text: "stream offline",
-
classes: "bg-gray-100 text-gray-800 border-gray-200",
},
};
···
<!-- connecting spinner -->
{#if status === "connecting"}
<div
-
class="animate-spin rounded-full h-4 w-4 border-b-2 border-yellow-800"
></div>
{/if}
<!-- status text -->
···
const statusConfig = {
connected: {
text: "stream live",
+
classes:
+
"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 border-green-200 dark:border-green-800",
},
connecting: {
text: "stream connecting",
+
classes:
+
"bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 border-yellow-200 dark:border-yellow-800",
},
error: {
text: "stream errored",
+
classes:
+
"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 border-red-200 dark:border-red-800",
},
disconnected: {
text: "stream offline",
+
classes:
+
"bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 border-gray-200 dark:border-gray-800",
},
};
···
<!-- connecting spinner -->
{#if status === "connecting"}
<div
+
class="animate-spin rounded-full h-4 w-4 border-b-2 border-yellow-800 dark:border-yellow-200"
></div>
{/if}
<!-- status text -->
+15 -9
client/src/routes/+page.svelte
···
/>
</svelte:head>
-
<header class="border-gray-300 border-b mb-4 pb-2">
<div
class="px-2 md:ml-[19vw] mx-auto flex flex-wrap items-center text-center"
>
-
<h1 class="text-4xl font-bold mr-4 text-gray-900">lexicon tracker</h1>
-
<p class="text-lg mt-1 text-gray-600">
tracks lexicons seen on the jetstream {tracking_since === 0
? ""
: `(since: ${formatTimestamp(tracking_since)})`}
</p>
</div>
</header>
-
<div class="md:max-w-[61vw] mx-auto p-2">
<div class="min-w-fit grid grid-cols-2 xl:grid-cols-4 gap-2 2xl:gap-6 mb-8">
<StatsCard
title="total creation"
···
{#if error}
<div
-
class="bg-red-100 border border-red-300 text-red-700 px-4 py-3 rounded-lg mb-6"
>
<p>Error: {error}</p>
</div>
···
{#if eventsList.length > 0}
<div class="mb-8">
<div class="flex flex-wrap items-center gap-3 mb-3">
-
<h2 class="text-2xl font-bold text-gray-900">seen lexicons</h2>
<StatusBadge status={websocketStatus} />
</div>
<div class="flex flex-wrap items-center gap-1.5 mb-6">
···
{/if}
</div>
-
<footer class="py-2 border-t border-gray-200 text-center">
-
<p class="text-gray-600 text-sm">
source code <a
href="https://tangled.sh/@poor.dog/nsid-tracker"
target="_blank"
rel="noopener noreferrer"
-
class="text-blue-600 hover:text-blue-800 underline"
>@poor.dog/nsid-tracker</a
>
</p>
···
/>
</svelte:head>
+
<header
+
class="bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-950 border-b mb-4 pb-2"
+
>
<div
class="px-2 md:ml-[19vw] mx-auto flex flex-wrap items-center text-center"
>
+
<h1 class="text-4xl font-bold mr-4 text-gray-900 dark:text-gray-200">
+
lexicon tracker
+
</h1>
+
<p class="text-lg mt-1 text-gray-600 dark:text-gray-300">
tracks lexicons seen on the jetstream {tracking_since === 0
? ""
: `(since: ${formatTimestamp(tracking_since)})`}
</p>
</div>
</header>
+
<div class="bg-white dark:bg-gray-900 md:max-w-[61vw] mx-auto p-2">
<div class="min-w-fit grid grid-cols-2 xl:grid-cols-4 gap-2 2xl:gap-6 mb-8">
<StatsCard
title="total creation"
···
{#if error}
<div
+
class="bg-red-100 dark:bg-red-900 border border-red-300 dark:border-red-700 text-red-700 dark:text-red-200 px-4 py-3 rounded-lg mb-6"
>
<p>Error: {error}</p>
</div>
···
{#if eventsList.length > 0}
<div class="mb-8">
<div class="flex flex-wrap items-center gap-3 mb-3">
+
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-200">
+
seen lexicons
+
</h2>
<StatusBadge status={websocketStatus} />
</div>
<div class="flex flex-wrap items-center gap-1.5 mb-6">
···
{/if}
</div>
+
<footer class="py-2 border-t border-gray-200 dark:border-gray-800 text-center">
+
<p class="text-gray-600 dark:text-gray-200 text-sm">
source code <a
href="https://tangled.sh/@poor.dog/nsid-tracker"
target="_blank"
rel="noopener noreferrer"
+
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-600 underline"
>@poor.dog/nsid-tracker</a
>
</p>