Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
519 changes: 515 additions & 4 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"devDependencies": {
"@eslint/compat": "^2.1.0",
"@eslint/js": "^10.0.1",
"@fontsource-variable/inter": "^5.3.0",
"@fontsource/poppins": "^5.3.0",
"@graphql-codegen/cli": "^6.3.1",
"@graphql-codegen/client-preset": "^5.3.0",
"@iconify-json/lucide": "^1.2.123",
Expand Down Expand Up @@ -55,6 +55,7 @@
"private": true,
"type": "module",
"dependencies": {
"@scalar/api-reference": "^1.65.1",
"@urql/exchange-graphcache": "^9.0.1",
"@urql/svelte": "^5.0.0",
"mode-watcher": "^1.1.0",
Expand Down
40 changes: 22 additions & 18 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@

@import "shadcn-svelte/tailwind.css";

@import "@fontsource-variable/inter";
/* Latin subsets only; the full imports also pull Devanagari, which this UI never renders. */
@import "@fontsource/poppins/latin-400.css";
@import "@fontsource/poppins/latin-500.css";
@import "@fontsource/poppins/latin-600.css";
@import "@fontsource/poppins/latin-700.css";

@custom-variant dark (&:is(.dark *));

Expand All @@ -20,18 +24,18 @@
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--primary: oklch(0.546 0.245 262.9);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.985 0.007 268.5);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted: oklch(0.985 0.007 268.5);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent: oklch(0.985 0.007 268.5);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--ring: oklch(0.546 0.245 262.9);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
Expand All @@ -48,30 +52,30 @@
}

.dark {
--background: oklch(0.145 0 0);
--background: oklch(0.178 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card: oklch(0.216 0.004 229);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover: oklch(0.216 0.004 229);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--primary: oklch(0.623 0.214 259.8);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.26 0.004 229);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted: oklch(0.26 0.004 229);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent: oklch(0.26 0.004 229);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--ring: oklch(0.623 0.214 259.8);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar: oklch(0.216 0.004 229);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
Expand Down Expand Up @@ -117,7 +121,7 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: "Inter Variable", sans-serif;
--font-sans: "Poppins", sans-serif;
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
Expand Down
12 changes: 12 additions & 0 deletions src/lib/components/custom/CardLink.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { ChevronRightIcon } from "@lucide/svelte";
import type { Snippet } from "svelte";

let { href, children }: { href: string; children: Snippet } = $props();
</script>

<Button {href} variant="outline" class="group h-auto w-full justify-between rounded-lg bg-card px-4 py-3.5 hover:border-primary/60 hover:bg-secondary">
<h3 class="text-base font-medium">{@render children()}</h3>
<ChevronRightIcon class="text-muted-foreground transition-transform group-hover:translate-x-0.5 group-hover:text-primary" />
</Button>
33 changes: 21 additions & 12 deletions src/lib/components/custom/header/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { AUTH_CTX } from "$lib/auth.svelte";
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { asset, resolve } from "$app/paths";
import { page } from "$app/state";
import { type HeaderProps } from "$lib/components/custom/header/index.svelte";
import { CircleUserIcon, LogOutIcon, WebhookIcon } from "@lucide/svelte";
import { BookOpenIcon, CircleUserIcon, LogOutIcon, WebhookIcon } from "@lucide/svelte";

let { breadcrumbs }: HeaderProps = $props();
const auth = AUTH_CTX.get();
Expand All @@ -16,15 +16,24 @@
</script>

<div>
<div class="flex justify-between">
<h1 class="text-3xl">
<div class="flex flex-wrap items-center justify-between gap-3">
<h1 class="text-3xl font-semibold tracking-tight">
<a
class="rounded-sm text-inherit no-underline transition-colors hover:text-muted-foreground focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none"
href={resolve("/")}>Fill</a
class="flex items-center gap-2.5 rounded-sm text-inherit no-underline transition-colors hover:text-muted-foreground focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none"
href={resolve("/")}
>
<img src={asset("/android-chrome-192x192.png")} alt="" class="size-12 [image-rendering:pixelated]" />
Fill
</a>
</h1>
{#if auth.getUsername()}
<div class="flex items-center gap-2">
<div class="flex items-center gap-2">
{#if page.url.pathname !== resolve("/docs")}
<Button variant="outline" size="sm" href={resolve("/docs")}>
<BookOpenIcon data-icon="inline-start" />
API docs
</Button>
{/if}
{#if auth.getUsername()}
{#if page.url.pathname !== resolve("/webhooks")}
<Button variant="outline" size="sm" href={resolve("/webhooks")}>
<WebhookIcon data-icon="inline-start" />
Expand All @@ -50,10 +59,10 @@
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{:else}
<Button variant="outline" size="sm" href="/login?redirect={encodeURIComponent(loginRedirect)}">Login</Button>
{/if}
{:else}
<Button variant="outline" size="sm" href="/login?redirect={encodeURIComponent(loginRedirect)}">Login</Button>
{/if}
</div>
</div>
<Breadcrumb.Root>
<Breadcrumb.List class="text-lg">
Expand Down
8 changes: 6 additions & 2 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@
import { ModeWatcher } from "mode-watcher";
import { page } from "$app/state";
import { type LayoutProps } from "./$types";
import { resolve } from "$app/paths";
import Footer from "./Footer.svelte";
import { createAppHydrationScope } from "$lib/hydration-scope";
import { Toaster } from "$lib/components/ui/sonner";

let { children, data }: LayoutProps = $props();

// Scalar renders its own full-height, full-width layout, so the docs route skips the app container.
const isDocs = $derived(page.url.pathname === resolve("/docs"));

createAppHydrationScope();

const auth = new AuthHolder();
Expand Down Expand Up @@ -80,8 +84,8 @@

<ModeWatcher />
<Toaster />
<div class="mx-auto flex min-h-svh max-w-5xl flex-col">
<div class="p-6">
<div class={["flex min-h-svh flex-col", isDocs ? "" : "mx-auto max-w-5xl"]}>
<div class={isDocs ? "flex flex-1 flex-col" : "p-6"}>
{@render children?.()}
</div>
<Footer />
Expand Down
96 changes: 96 additions & 0 deletions src/routes/docs/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<script lang="ts">
import { mode } from "mode-watcher";
import { ElementSize } from "runed";
import Header from "$lib/components/custom/header/Header.svelte";
import { resolve } from "$app/paths";
import { API_ENDPOINT } from "$lib/api.svelte";
import scalarStyleUrl from "@scalar/api-reference/style.css?url";

let container: HTMLDivElement | undefined = $state();
let bar: HTMLDivElement | undefined = $state();
let loading = $state(true);
let failed = $state(false);

// Scalar offsets its sticky sidebar by this, so it tracks the real bar height instead of a hardcoded guess.
const barSize = new ElementSize(() => bar);

// Scalar's stylesheet sets a global body background, so it is attached here and detached on unmount.
$effect(() => {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = scalarStyleUrl;
document.head.append(link);
return () => {
link.remove();
document.body.classList.remove("dark-mode", "light-mode");
};
});

$effect(() => {
const target = container;
if (!target) return;

// Scalar resolves its color mode once during setup, so a theme change needs a fresh instance.
const forceDarkModeState = mode.current === "dark" ? "dark" : "light";
let reference: { destroy: () => void } | undefined;
let destroyed = false;

void (async () => {
try {
const { createApiReference } = await import("@scalar/api-reference");
if (destroyed) return;
reference = createApiReference(target, {
url: API_ENDPOINT + "/openapi.yaml",
forceDarkModeState,
showDeveloperTools: "never",
});
} catch {
failed = true;
} finally {
loading = false;
}
})();

return () => {
destroyed = true;
reference?.destroy();
};
});
</script>

<svelte:head>
<title>API docs - Fill</title>
</svelte:head>

<div class="flex flex-1 flex-col" style="--scalar-custom-header-height: {barSize.height}px">
<div bind:this={bar} class="sticky top-0 z-20 border-b bg-background/80 px-6 py-4 backdrop-blur-md">
<Header breadcrumbs={[{ label: "API docs", href: resolve("/docs") }]} />
</div>

{#if loading}
<p class="flex flex-1 items-center justify-center gap-2 p-6 text-sm text-muted-foreground">
<span class="iconify animate-spin lucide--loader-2"></span>
Loading API docs...
</p>
{:else if failed}
<p class="flex flex-1 items-center justify-center p-6 text-sm text-muted-foreground">Could not load the API docs. Reload the page to try again.</p>
{/if}

<div bind:this={container} class="flex-1"></div>
</div>

<style>
/* Bridge Scalar's palette onto the app tokens so the embed does not read as a second product. */
:global(body) {
--scalar-font: var(--font-sans);
--scalar-background-1: var(--background);
--scalar-background-2: var(--card);
--scalar-background-3: var(--secondary);
--scalar-background-accent: color-mix(in oklab, var(--primary) 15%, transparent);
--scalar-border-color: var(--border);
--scalar-color-1: var(--foreground);
--scalar-color-2: var(--muted-foreground);
--scalar-color-3: var(--muted-foreground);
--scalar-color-accent: var(--primary);
}
</style>
7 changes: 3 additions & 4 deletions src/routes/projects/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import { SHARED_QUERIES_CTX } from "$lib/api.svelte";
import Header from "$lib/components/custom/header/Header.svelte";
import { Button } from "$lib/components/ui/button";
import CardLink from "$lib/components/custom/CardLink.svelte";
import { resolve } from "$app/paths";
import * as Alert from "$lib/components/ui/alert";
import LoadingSniffer from "$lib/components/LoadingSniffer.svelte";
import { projectsHeaderSegment } from "$lib/components/custom/header/index.svelte";
Expand Down Expand Up @@ -37,9 +38,7 @@
{:else}
{#each safeProjects as project (project.key)}
<li>
<Button class="w-full justify-between" variant="outline" href={`/projects/${project.key}`}>
<h3 class="text-sm font-medium">{project.name}</h3>
</Button>
<CardLink href={resolve("/projects/[project]", { project: project.key })}>{project.name}</CardLink>
</li>
{/each}
{/if}
Expand Down
6 changes: 3 additions & 3 deletions src/routes/projects/[project]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { RunedQuery, SHARED_QUERIES_CTX } from "$lib/api.svelte";
import Header from "$lib/components/custom/header/Header.svelte";
import { page } from "$app/state";
import { resolve } from "$app/paths";
import CardLink from "$lib/components/custom/CardLink.svelte";
import { Button } from "$lib/components/ui/button";
import * as Alert from "$lib/components/ui/alert";
import { getContextClient, queryStore } from "@urql/svelte";
Expand Down Expand Up @@ -74,9 +76,7 @@
<ul class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each families as family (family.key)}
<li>
<Button class="w-full justify-between" variant="outline" href={`/projects/${page.params.project}/family/${family.key}`}>
<h3 class="text-sm font-medium">{family.key}</h3>
</Button>
<CardLink href={resolve("/projects/[project]/family/[family]", { project: page.params.project!, family: family.key })}>{family.key}</CardLink>
</li>
{/each}
</ul>
Expand Down