feat: scope + un-bundle the plugin; @unom/ui SPA; custom-emulator editor
CI / publish (push) Has been cancelled
CI / build (push) Has been cancelled

Reliability + correctness pass on the packaging and UI.

Packaging — drop the self-contained bundle:
- Rename to `@punktfunk/plugin-rom-manager` (scoped). An unscoped package can't
  be split across registries, which is the only reason bundling effect + the SDK
  into every plugin seemed necessary. Scoped names resolve from one scope-map, so
  the plugin depends on `@punktfunk/host` + `effect` as SHARED (hoisted) deps —
  no duplication, no ~1 MB effect copy per plugin. Build is back to plain `tsc`.
  (Runner-side discovery of `@punktfunk/plugin-*` shipped in @punktfunk/host 0.1.1.)

UI — use the console's design system so it reads as family:
- SPA rebuilt on @unom/style tokens + Tailwind v4 + the console's exact theme
  (violet chrome, Geist, card/button vocabulary). Build-time only → static assets
  in dist/ui, no @unom runtime dep. Skips @unom/ui's AnimatedButton/Card, which
  statically import ~7 MB of UI sound assets — same look, 416 KB dist/ui.
- Add a custom-emulator editor (Emulators page) writing config.emulators — you can
  now add a launcher Punktfunk doesn't ship a definition for.
- Fix brand capitalization to "Punktfunk" in all UI copy + docs.

Install docs + CI updated for the scoped `bun add @punktfunk/plugin-rom-manager`
flow. 48 engine tests green; backend + UI typecheck + biome clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-18 12:16:04 +02:00
parent aee3c194af
commit 33e91e4d00
25 changed files with 2298 additions and 831 deletions
+28
View File
@@ -0,0 +1,28 @@
// A small shadcn-style badge on the shared tokens (pill, brand/muted/state variants).
import { cva, type VariantProps } from "class-variance-authority";
import type { HTMLAttributes } from "react";
import { cn } from "../../lib/utils.js";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium transition-colors",
{
variants: {
variant: {
default: "border-transparent bg-secondary text-secondary-foreground",
brand: "border-transparent bg-primary/15 text-primary",
outline: "text-muted-foreground",
success: "border-success/40 text-success",
warn: "border-amber-500/40 text-amber-400",
},
},
defaultVariants: { variant: "default" },
},
);
export interface BadgeProps
extends HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof badgeVariants> {}
export const Badge = ({ className, variant, ...props }: BadgeProps) => (
<span className={cn(badgeVariants({ variant }), className)} {...props} />
);
+52
View File
@@ -0,0 +1,52 @@
// Button — the console's exact variant/size vocabulary + brand tokens (pill shape, `--primary` fill),
// but a plain <button> rather than @unom/ui's AnimatedButton: the animated one statically imports ~7 MB
// of UI click/hover sound assets (fine for the full console, absurd for an embedded iframe). Same look,
// no audio. `buttonVariants` here mirrors @unom/ui's so it reads identically.
import { cva, type VariantProps } from "class-variance-authority";
import type { ButtonHTMLAttributes } from "react";
import { cn } from "../../lib/utils.js";
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-button text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-transparent shadow-sm hover:bg-secondary hover:text-secondary-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-secondary hover:text-secondary-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-8",
icon: "size-9",
},
},
defaultVariants: { variant: "default", size: "default" },
},
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export const Button = ({
className,
variant,
size,
type = "button",
...props
}: ButtonProps) => (
<button
type={type}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
+56
View File
@@ -0,0 +1,56 @@
// Card — the console's card surface (bg-card, brand-violet ring, rounded-card) as a plain element set,
// on the shared @unom tokens. We skip @unom/ui's AnimatedCard (motion + material) to keep the bundle
// lean; the look matches because the tokens (colour/radius/border) are identical.
import type { HTMLAttributes } from "react";
import { cn } from "../../lib/utils.js";
export const Card = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"rounded-card border border-border bg-card text-card-foreground shadow-sm ring-1 ring-accent/30",
className,
)}
{...props}
/>
);
export const CardHeader = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
);
export const CardTitle = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<h2
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
);
export const CardDescription = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<p className={cn("text-sm text-muted-foreground", className)} {...props} />
);
export const CardContent = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("p-6 pt-0", className)} {...props} />
);
export const CardFooter = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex items-center p-6 pt-0", className)} {...props} />
);
+23
View File
@@ -0,0 +1,23 @@
// shadcn-style input + select on the shared tokens, so form controls match the console's inputs.
import type { InputHTMLAttributes, SelectHTMLAttributes } from "react";
import { cn } from "../../lib/utils.js";
const base =
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50";
export const Input = ({
className,
...props
}: InputHTMLAttributes<HTMLInputElement>) => (
<input className={cn(base, className)} {...props} />
);
export const Select = ({
className,
children,
...props
}: SelectHTMLAttributes<HTMLSelectElement>) => (
<select className={cn(base, "cursor-pointer", className)} {...props}>
{children}
</select>
);