feat: scope + un-bundle the plugin; @unom/ui SPA; custom-emulator editor
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:
@@ -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} />
|
||||
);
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
@@ -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} />
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
Reference in New Issue
Block a user