Files
officer c11f45c033 soulseek, extracted from the platform as a plugin
Seventh plugin out, and the first that was already dead. Soulseek had been
switched off since 2026-08-13: hono.ts had the router import and its mount
commented out, so createSidecarProxy never ran, registerProxiedPrefix was never
called, the slskd:server announcement fired into a void, and officer-slskd was
not in the ecosystem at all. The permission entry had been reduced to api: [],
which is why assertPermissionTotality was satisfied and the server booted
clean. The UI rendered perfectly and every request 404'd.

So this is a resurrection as well as a move.

The mount is now /api/soulseek, derived via mountPrefix from the appName — all
47 frontend call sites repointed from /slskd/. The credential key in
service_connections stays 'slskd': it names the daemon, not the route. Those
two strings differ on purpose and conflating them reads the wrong row.

The connection stays in core service_connections, as transmission does, so
'slskd' remains in a platform union this plugin cannot extend. Recorded in
PLUGIN.md. Unlike transmission this plugin does own a schema: favourites and
the two halves of the cached browse tree, which slskd has no concept of. Those
three tables had been dropped from the drizzle barrel, so bun db:push would
have deleted them on any server holding data.

313 palette classes converted to the semantic tone set, including a shared
PHASE_TONE the three transfer views previously each copied.
2026-08-19 02:55:02 +00:00

128 lines
4.5 KiB
TypeScript

import type { ReactNode } from 'react';
import { ChevronRight } from 'lucide-react';
// Shared visual language for the /soulseek grouped views (search results + downloads). Each top-level
// grouping (a peer/user) is an almost-black Card; each folder within it is a lifted SubCard. Kept here
// so search and downloads stay pixel-identical — change the look in one place.
export const Card = ({ children }: { children: ReactNode }) => (
<div className="overflow-hidden rounded-xl border border-border bg-card shadow-sm">{children}</div>
);
// Supply exactly one of `title` (inert text — the whole row becomes the collapse toggle) or `titleMenu`
// (an interactive node such as the peer dropdown). A trigger can't live inside the toggle button, so with
// `titleMenu` the row splits: the chevron and the empty space after the node toggle, the node itself doesn't.
type CardHeaderProps = {
open: boolean;
onToggle: () => void;
title?: ReactNode;
meta?: ReactNode;
titleMenu?: ReactNode;
};
export const CardHeader = ({ open, onToggle, title, meta, titleMenu }: CardHeaderProps) => {
const chevron = (
<ChevronRight className={`h-4 w-4 shrink-0 text-muted-foreground transition-transform ${open ? 'rotate-90' : ''}`} />
);
const metaBlock = meta && <div className="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">{meta}</div>;
if (!titleMenu)
return (
<button
type="button"
onClick={onToggle}
className="flex w-full items-center gap-2.5 px-3 py-2.5 text-left transition-colors hover:bg-muted"
>
{chevron}
<span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">{title}</span>
{metaBlock}
</button>
);
return (
<div className="flex w-full items-center gap-2.5 px-3 py-2.5 transition-colors hover:bg-muted">
<button
type="button"
onClick={onToggle}
aria-expanded={open}
aria-label={open ? 'Collapse' : 'Expand'}
className="flex shrink-0 items-center"
>
{chevron}
</button>
{titleMenu}
<button type="button" onClick={onToggle} tabIndex={-1} aria-hidden className="h-6 min-w-0 flex-1" />
{metaBlock}
</div>
);
};
export const CardBody = ({ children }: { children: ReactNode }) => (
<div className="flex flex-col gap-1.5 border-t border-border bg-muted/30 p-2">{children}</div>
);
export const SubCard = ({ children }: { children: ReactNode }) => (
<div className="overflow-hidden rounded-lg border border-border/50 bg-muted/20">{children}</div>
);
// A folder header. When `onToggle` is supplied it becomes a collapse control (chevron + clickable
// label); `action` stays a sibling so it never nests inside the toggle button.
type SubCardHeaderProps = {
icon?: ReactNode;
label: ReactNode;
title?: string;
meta?: ReactNode;
action?: ReactNode;
open?: boolean;
onToggle?: () => void;
};
export const SubCardHeader = ({ icon, label, title, meta, action, open, onToggle }: SubCardHeaderProps) => {
const inner = (
<>
{onToggle && (
<ChevronRight className={`h-3 w-3 shrink-0 text-muted-foreground transition-transform ${open ? 'rotate-90' : ''}`} />
)}
{icon}
<span className="min-w-0 flex-1 truncate text-xs text-muted-foreground" title={title}>
{label}
</span>
{meta && <span className="shrink-0 text-xs text-muted-foreground">{meta}</span>}
</>
);
return (
<div className="flex items-center gap-2 bg-muted/30 px-2.5 py-1.5">
{onToggle ? (
<button
type="button"
onClick={onToggle}
className="flex min-w-0 flex-1 items-center gap-2 text-left transition-colors hover:text-foreground"
>
{inner}
</button>
) : (
inner
)}
{action}
</div>
);
};
// File rows inside a SubCard, hairline-divided.
export const RowList = ({ children }: { children: ReactNode }) => (
<div className="divide-y divide-border/50">{children}</div>
);
type PillProps = { active: boolean; onClick: () => void; children: ReactNode };
export const Pill = ({ active, onClick, children }: PillProps) => (
<button
type="button"
onClick={onClick}
className={`inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs transition ${
active
? 'border-primary/40 bg-primary/10 text-primary'
: 'border-border bg-muted/20 text-muted-foreground hover:bg-muted hover:text-foreground'
}`}
>
{children}
</button>
);