audiorouter-dashboard 0.2.0

HTTP/SSE dashboard API and static-file host for audiorouter
import type { ValidationError, ValidationWarning } from "../lib/api";

type ValidationIssue = ValidationError | ValidationWarning;

interface Props {
  errors: ValidationError[];
  warnings: ValidationWarning[];
  onIssueClick?: (issue: ValidationIssue) => void;
}

export function ValidationPanel({ errors, warnings, onIssueClick }: Props) {
  return (
    <div className="h-full">
      {errors.length === 0 && warnings.length === 0 ? (
        <div className="flex h-full items-center justify-center text-center">
          <span className="text-sm font-medium" style={{ color: "var(--color-ar-in)" }}>
            ✓ 設定は有効です
          </span>
        </div>
      ) : (
        <div className="space-y-3">
          {errors.length > 0 && (
            <IssueGroup
              title={`Errors (${errors.length})`}
              toneColor="var(--color-destructive)"
              icon="✕"
              issues={errors}
              onIssueClick={onIssueClick}
            />
          )}

          {warnings.length > 0 && (
            <IssueGroup
              title={`Warnings (${warnings.length})`}
              toneColor="var(--color-ar-gain)"
              icon="⚠"
              issues={warnings}
              onIssueClick={onIssueClick}
            />
          )}
        </div>
      )}
    </div>
  );
}

function IssueGroup({
  title,
  toneColor,
  icon,
  issues,
  onIssueClick,
}: {
  title: string;
  toneColor: string;
  icon: string;
  issues: ValidationIssue[];
  onIssueClick?: (issue: ValidationIssue) => void;
}) {
  return (
    <div>
      <p className="mb-1.5 text-xs font-semibold" style={{ color: toneColor }}>
        {title}
      </p>
      <ul className="space-y-1">
        {issues.map((issue, i) => (
          <li key={`${issue.path}:${issue.message}:${i}`}>
            <div
              role="button"
              tabIndex={0}
              className="validation-issue flex w-full cursor-pointer items-start gap-2 rounded-md p-2 text-left text-xs transition hover:bg-[var(--color-muted)]"
              style={{
                background: `color-mix(in oklch, ${toneColor} 10%, transparent)`,
              }}
              title="クリックして該当するノードまたはパスを選択"
              onClick={() => {
                if (window.getSelection()?.toString()) return;
                onIssueClick?.(issue);
              }}
              onKeyDown={(e) => {
                if (e.key === "Enter" || e.key === " ") {
                  e.preventDefault();
                  onIssueClick?.(issue);
                }
              }}
            >
              <span className="shrink-0" style={{ color: toneColor }}>
                {icon}
              </span>
              <span className="min-w-0 flex-1">
                {issue.path && (
                  <code className="mr-2 font-mono text-[var(--color-muted-foreground)]">
                    {issue.path}
                  </code>
                )}
                <span className="text-[var(--color-foreground)]">{issue.message}</span>
              </span>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}