Files
punktfunk/web/src/sections/Store/TierBadge.tsx
T
enricobuehlerandClaude Fable 5 833f3348a0 feat(store): console plugin store, index repo, and the fixes on-glass found
Console: a Plugins section (Browse / Installed / Sources) on a static nav
entry, with install friction proportional to trust — a plain confirm for a
verified entry, a warning naming the curator for an external one, and a
danger dialog that makes you retype the spec for a raw package. Tier badges
are permanent and follow the plugin onto its own UI page.

Index: unom/punktfunk-plugin-index published and served from Gitea's raw
endpoint (real HTTPS, byte-exact, no vhost to stand up) — merge to main is
publish, which resolves the design's open hosting question.

Four things only running it could find:
- runner discovery matched @punktfunk/plugin-* only, so a third-party
  scoped plugin (which D8 requires) would install and never run
- ...and that convention also matches @punktfunk/plugin-kit, a plugin's
  own framework: it listed as installed and would have been imported as a
  unit. Both now key off the plugins dir's top-level dependencies, with an
  emptied dependency list meaning 'nothing installed' rather than falling
  back to the naming convention
- the store must not pass new flags to the runner: the scripting package
  ships separately and an older one reads an unknown flag's value as a
  package name. The host writes the bunfig scope mapping itself
- ureq reports only >= 400 as Err, so a conditional request's 304 arrives
  as Ok with an empty body — handled as an error it made every refresh
  after the first verify a signature over zero bytes and sit stale

Also: the console's first Tabs use exposed an @unom/ui theme gap that
rendered inactive tabs invisible (caught in a browser pass, not by types).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 20:48:02 +02:00

90 lines
2.4 KiB
TypeScript

import {
BadgeCheck,
ShieldAlert,
ShieldQuestion,
Terminal,
} from "lucide-react";
import type { FC } from "react";
import type { StoreTier } from "@/api/store";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { m } from "@/paraglide/messages";
// The trust model, rendered. These badges are the only place the console makes a claim about where
// a plugin's code came from, so they are deliberately literal: ONLY the built-in `unom` source earns
// the check mark, everything else says out loud that nobody at unom looked at the code. An entry
// from an operator-added source additionally carries a <SourceChip> naming that source — attribution
// instead of a verification it hasn't got.
/** The short, permanent provenance badge for a tier. */
export const TierBadge: FC<{ tier: StoreTier; className?: string }> = ({
tier,
className,
}) => {
switch (tier) {
case "verified":
return (
<Badge
variant="success"
className={cn("gap-1", className)}
title={m.store_tier_verified_hint()}
>
<BadgeCheck className="size-3.5" />
{m.store_tier_verified()}
</Badge>
);
case "external":
return (
<Badge
variant="outline"
className={cn(
"gap-1 border-amber-600/40 text-amber-600 dark:border-amber-500/40 dark:text-amber-500",
className,
)}
title={m.store_tier_external_hint()}
>
<ShieldQuestion className="size-3.5" />
{m.store_tier_external()}
</Badge>
);
case "unverified":
return (
<Badge
variant="destructive"
className={cn("gap-1", className)}
title={m.store_tier_unverified_hint()}
>
<ShieldAlert className="size-3.5" />
{m.store_tier_unverified()}
</Badge>
);
default:
return (
<Badge
variant="secondary"
className={cn("gap-1", className)}
title={m.store_tier_cli_hint()}
>
<Terminal className="size-3.5" />
{m.store_tier_cli()}
</Badge>
);
}
};
/** "from <source>" — who curated this entry. Shown for anything not from the built-in source. */
export const SourceChip: FC<{ source: string; className?: string }> = ({
source,
className,
}) => (
<span
className={cn(
"inline-flex items-center gap-1 text-xs text-muted-foreground",
className,
)}
>
{m.store_from_source()}
<span className="font-medium text-foreground">{source}</span>
</span>
);