feat(shell,api,keys): firefly v1 shell over the api with cookie auth, period, palette and keymap

This commit is contained in:
hh committed 2026-10-16 02:21:45 +00:00
1 parent ae4de7911b
commit c74dac3e00
37 files changed
+58163 -79

No files matched your search

+84 -1
View File
@@ -1,10 +1,93 @@
<script lang="ts">
import "./layout.css";
import { QueryClientProvider } from "@tanstack/svelte-query";
import { ModeWatcher } from "mode-watcher";
import { onMount, untrack } from "svelte";
import { queryClient } from "#lib/api/lists.js";
import QuickEntry from "#lib/components/QuickEntry.svelte";
import Shell from "#lib/components/shell/Shell.svelte";
import { keys } from "#lib/keys.svelte.js";
import { period, rangeFromUrl } from "#lib/period.svelte.js";
import { session } from "#lib/session.svelte.js";
import { goto } from "$app/navigation";
import { page } from "$app/state";
let { children } = $props();
let lastHref = "";
onMount(async () => {
try {
const back = await session.boot();
period.init(session.viewRange, page.url);
if (back) {
await goto(back, { replace: true });
}
} catch (error) {
session.failed = error instanceof Error ? error.message : String(error);
}
});
$effect(() => {
if (!period.ready) {
return;
}
const url = page.url;
const start = period.start;
const end = period.end;
untrack(() => {
const fromUrl = rangeFromUrl(url);
if (url.href !== lastHref && fromUrl) {
lastHref = url.href;
period.set(fromUrl);
return;
}
lastHref = url.href;
if (fromUrl?.start === start && fromUrl?.end === end) {
return;
}
const next = new URL(url.href);
next.searchParams.set("start", start);
next.searchParams.set("end", end);
lastHref = next.href;
goto(next, { replace: true, shallow: true, state: page.state });
});
});
</script>
<ModeWatcher />
<svelte:window onkeydown={keys.handle} />
{@render children()}
<QueryClientProvider client={queryClient}>
{#if session.ready}
<Shell>
{@render children()}
{#snippet quick()}
<QuickEntry />
{/snippet}
</Shell>
{:else if session.failed}
<main class="grid min-h-dvh place-items-center p-6 text-center">
<div class="flex max-w-md flex-col gap-3">
<h1 class="font-semibold text-[18px]">Firefly III did not answer</h1>
<p class="text-cladd-fg-soft">{session.failed}</p>
<p class="flex justify-center gap-4">
<a class="text-brand underline" href={page.url.pathname}>Try again</a>
<a class="text-brand underline" data-sveltekit-reload href="/login"
>Log in</a
>
<a
class="text-brand underline"
data-sveltekit-reload
href={`${page.url.pathname}?ffui=old`}
>Open the old interface</a
>
</p>
</div>
</main>
{:else}
<main class="grid min-h-dvh place-items-center text-cladd-fg-soft">
<span>Connecting to Firefly III</span>
</main>
{/if}
</QueryClientProvider>
+4 -12
View File
@@ -1,13 +1,5 @@
<svelte:head>
<title>Firefly UI</title>
</svelte:head>
<script lang="ts">
import PageHeader from "#lib/components/shell/PageHeader.svelte";
</script>
<main
class="flex min-h-dvh flex-col items-center justify-center gap-4 px-6 text-center bg-background text-foreground"
>
<h1 class="font-bold text-4xl tracking-tight sm:text-5xl">Firefly UI</h1>
<p class="max-w-prose text-base text-muted-foreground">
Edit
<code class="font-mono">src/routes/+page.svelte</code>.
</p>
</main>
<PageHeader subtitle="What's playing?" title="Dashboard" />
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from "@sveltejs/kit";
export function load() {
redirect(307, "/");
}
+100
View File
@@ -5,11 +5,111 @@
@import "./claddcn.css";
@custom-variant dark (&:is(.dark *));
:root {
--ff-brand: oklch(0.57 0.105 240);
--ff-brand-deep: oklch(0.5 0.1 240);
--ff-on-brand: oklch(0.99 0.01 240);
--ff-income: oklch(0.5 0.12 145);
--ff-expense: oklch(0.52 0.15 28);
--ff-transfer: oklch(0.5 0.085 225);
--ff-warn: oklch(0.62 0.14 70);
--ff-row-hover: oklch(0.96 0.012 240);
--ff-row-cursor: oklch(0.93 0.03 240);
--ff-row-selected: oklch(0.9 0.05 240);
--ff-series-1: oklch(0.57 0.105 240);
--ff-series-2: oklch(0.62 0.13 160);
--ff-series-3: oklch(0.7 0.14 70);
--ff-series-4: oklch(0.58 0.15 20);
--ff-series-5: oklch(0.55 0.12 300);
--ff-series-6: oklch(0.65 0.1 200);
--ff-series-7: oklch(0.6 0.13 110);
--ff-series-8: oklch(0.6 0.08 260);
}
.cladd-color-brand {
--cladd-theme: #3c8dbc;
}
.dark {
--ff-brand: oklch(0.42 0.085 240);
--ff-brand-deep: oklch(0.36 0.075 240);
--ff-on-brand: oklch(0.97 0.01 240);
--ff-income: oklch(0.78 0.15 140);
--ff-expense: oklch(0.76 0.1 35);
--ff-transfer: oklch(0.76 0.08 220);
--ff-warn: oklch(0.8 0.13 80);
--ff-row-hover: oklch(0.25 0.012 240);
--ff-row-cursor: oklch(0.29 0.03 240);
--ff-row-selected: oklch(0.33 0.06 240);
--ff-series-1: oklch(0.7 0.1 240);
--ff-series-2: oklch(0.72 0.12 160);
--ff-series-3: oklch(0.8 0.13 75);
--ff-series-4: oklch(0.7 0.13 25);
--ff-series-5: oklch(0.7 0.11 300);
--ff-series-6: oklch(0.75 0.09 200);
--ff-series-7: oklch(0.75 0.12 115);
--ff-series-8: oklch(0.72 0.07 260);
}
@theme inline {
--color-brand: var(--ff-brand);
--color-brand-deep: var(--ff-brand-deep);
--color-on-brand: var(--ff-on-brand);
--color-income: var(--ff-income);
--color-expense: var(--ff-expense);
--color-transfer: var(--ff-transfer);
--color-warn: var(--ff-warn);
--color-row-hover: var(--ff-row-hover);
--color-row-cursor: var(--ff-row-cursor);
--color-row-selected: var(--ff-row-selected);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
scrollbar-color: var(--cladd-fg-softest) transparent;
}
body {
font-size: 13px;
line-height: 1.45;
@apply bg-background text-foreground;
}
::selection {
background: color-mix(in oklch, var(--ff-brand) 28%, transparent);
}
:focus-visible {
outline: 1.5px solid var(--ff-brand);
outline-offset: 1px;
}
.dark :focus-visible {
outline-color: var(--ff-series-1);
}
a {
text-underline-offset: 3px;
}
}
@utility num {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
white-space: nowrap;
}
@utility kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.35rem;
height: 1.25rem;
padding: 0 0.3rem;
font-family: var(--font-mono);
font-size: 11px;
line-height: 1;
color: var(--cladd-fg-soft);
background: var(--cladd-surface-cut);
border-radius: 4px;
box-shadow: inset 0 -1px 0 var(--cladd-outline);
}