Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
@@ -0,0 +1,158 @@
<script lang="ts">
import { formatCredits } from "$lib/utils";
import {
formatCreditsRemaining,
isEnterprisePlan,
isPayAsYouGoPlan,
type PlanLike
} from "$lib/billing-display";
import { i18n } from "$lib/i18n";
import { FolderTree, ListTree, Package, Rss, Sparkles } from "@lucide/svelte";
let {
stats,
credits,
plan = null
}: {
stats: {
products: number;
categories: number;
attributes: number;
feeds: number;
processed?: number;
unprocessed?: number;
};
credits: { usedCredits: string | number; remainingCredits: string | number };
plan?: PlanLike | null;
} = $props();
const remainingNum = $derived(
typeof credits.remainingCredits === "number"
? credits.remainingCredits
: Number(credits.remainingCredits)
);
const remainingLabel = $derived(formatCreditsRemaining(remainingNum, plan));
const isPayg = $derived(isPayAsYouGoPlan(plan));
const isEnterprise = $derived(isEnterprisePlan(plan));
const productsHint = $derived.by(() => {
const processed = stats.processed;
const unprocessed = stats.unprocessed;
if (
typeof processed === "number" &&
typeof unprocessed === "number" &&
(processed > 0 || unprocessed > 0)
) {
return i18n.t("stats.productsHint", {
processed: processed.toLocaleString(),
unprocessed: unprocessed.toLocaleString()
});
}
return i18n.t("stats.openCatalog");
});
const cards = $derived([
{
href: "/products",
label: i18n.t("stats.products"),
value: stats.products,
hint: productsHint,
iconWrap: "bg-card-blue text-text",
icon: Package,
tour: "stats-products"
},
{
href: "/categories",
label: i18n.t("stats.categories"),
value: stats.categories,
hint: i18n.t("stats.catalogTree"),
iconWrap: "bg-muted text-text",
icon: FolderTree,
tour: "stats-categories"
},
{
href: "/attributes",
label: i18n.t("stats.attributes"),
value: stats.attributes,
hint: i18n.t("stats.attributeLibrary"),
iconWrap: "bg-muted text-text",
icon: ListTree,
tour: "stats-attributes"
},
{
href: "/feeds",
label: i18n.t("stats.feeds"),
value: stats.feeds,
hint: i18n.t("stats.importSources"),
iconWrap: "bg-muted text-text",
icon: Rss,
tour: "stats-feeds"
}
]);
</script>
<div
class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5"
data-tour="dashboard-stats"
>
{#each cards as card (card.tour)}
<a
href={card.href}
class="group rounded-xl border border-border bg-surface text-text shadow-sm transition hover:border-ring/40 hover:bg-accent/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
data-tour={card.tour}
>
<div class="flex items-start justify-between gap-2 p-4 pb-1">
<div class="min-w-0">
<p class="text-xs font-medium text-text-muted">{card.label}</p>
<p class="mt-1 text-2xl font-semibold tracking-tight text-text">
{card.value.toLocaleString()}
</p>
</div>
<div
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg {card.iconWrap}"
aria-hidden="true"
>
<card.icon class="h-4 w-4" />
</div>
</div>
<div class="px-4 pb-3 text-xs text-text-muted group-hover:text-text/80">
{card.hint}
</div>
</a>
{/each}
<a
href="/billing"
class="group rounded-xl border border-border bg-surface text-text shadow-sm transition hover:border-ring/40 hover:bg-accent/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
data-tour="stats-credits"
>
<div class="flex items-start justify-between gap-2 p-4 pb-1">
<div class="min-w-0">
<p class="text-xs font-medium text-text-muted">{i18n.t("stats.credits")}</p>
<p class="mt-1 text-2xl font-semibold tracking-tight text-text">
{#if isPayg}
{i18n.t("stats.payAsYouGo")}
{:else}
{remainingLabel}
{/if}
</p>
</div>
<div
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-success/15 text-success"
aria-hidden="true"
>
<Sparkles class="h-4 w-4" />
</div>
</div>
<div class="px-4 pb-3 text-xs text-text-muted group-hover:text-text/80">
{#if isPayg}
{i18n.t("stats.walletBilling")}
{:else if isEnterprise}
{i18n.t("stats.enterpriseBilling")}
{:else}
{i18n.t("stats.usedBilling", { used: formatCredits(credits.usedCredits) })}
{/if}
</div>
</a>
</div>