rustlanges-components 0.1.0

RustLangES components library
Documentation
import { ShowcaseDef } from ".";
import { prepareProps, ShowcaseField } from "./field";
import { ChevronDown } from "./icons";
import { createSignal, h, MiniUI } from "./miniui";
import { renderErrors } from "./error";

const registeredCases: InternalCase<unknown>[] = [];

export function getRegisteredCases(): InternalCase<unknown>[] {
  return registeredCases;
}

export type PropDef = {
  displayName?: string;
  disabled?: boolean;
  hidden?: boolean;
  optional?: boolean;
  isSlot?: boolean;
} & (
  | {
      kind: "raw";
      value: unknown;
      default?: any;
      options?: never[];
    }
  | {
      kind: "boolean";
      default?: boolean;
      options?: never[];
    }
  | {
      kind: "callback";
      default?: any;
      options?: never[];
    }
  | {
      kind: "function";
      default?: boolean;
      options?: never[];
    }
  | {
      kind: "icon";
      default?: any;
      options?: never[];
    }
  | {
      kind: "number";
      default?: number;
      options?: number[];
    }
  | {
      kind: "string";
      default?: string;
      options?: string[];
    }
);

export type PropKind = PropDef["kind"];

export interface CaseDef<TComponent> {
  props: Record<string, Exclude<PropKind, "raw"> | PropDef>;
  slots?: Record<
    string,
    | Exclude<PropKind, "raw" | "function" | "callback">
    | Exclude<PropDef, { kind: "function" | "callback" }>
  >;
  component: TComponent;
}

export type InternalCase<TComponent> =
  | {
      title: string;
      kind: "render";
      render: TComponent;
    }
  | {
      title: string;
      kind: "def";
      def: CaseDef<TComponent>;
    };

export function registerCase<TNode>(title: string, def: CaseDef<TNode>): void;
export function registerCase<TNode>(title: string, render: () => TNode): void;

export function registerCase<TNode>(
  title: string,
  defOrRender: CaseDef<TNode> | (() => TNode)
): void {
  if (typeof defOrRender === "function") {
    registeredCases.push({
      title,
      kind: "render",
      render: defOrRender,
    });
  } else {
    registeredCases.push({
      title,
      kind: "def",
      def: defOrRender,
    });
  }
}

export function ShowCase<TComponent, TNode>({
  showcaseDef,
  caseDef,
}: {
  showcaseDef: ShowcaseDef<TComponent, TNode>;
  caseDef: InternalCase<TComponent>;
}): MiniUI.Node {
  let renderer: () => TNode;
  let isDefRenderer = false;
  if (caseDef.kind === "render") {
    renderer = () => showcaseDef.instiate(caseDef.render, {});
  } else {
    isDefRenderer = true;
    renderer = () => ShowCaseDef(caseDef.def, showcaseDef);
  }

  const caseFailed = createSignal(true);

  return (
    <details
      class={() => [
        "shadow-brutal details-content:flex max-w-case mx-auto mb-5 px-3 py-2",
        "border-1 rounded-sm border-black",
        !caseFailed() && "bg-light dark:bg-neutral-900",
        caseFailed() && "bg-error-400 dark:bg-error-600",
      ]}
    >
      <summary
        class={[
          "flex list-none items-center justify-between pr-3 text-lg marker:hidden",
          "in-open:border-b-1 border-b-0 border-b-neutral-300 dark:border-b-neutral-700",
        ]}
      >
        <span class="flex items-center gap-2">{caseDef.title}</span>
        <span class="in-open:rotate-0 rotate-[-90deg] text-2xl transition">
          <ChevronDown />
        </span>
      </summary>
      <div
        class={() => [
          "flex",
          !caseFailed() &&
            isDefRenderer &&
            "w-full flex-col items-center gap-2",
          !caseFailed() && !isDefRenderer && "items-center justify-center pt-2",
          caseFailed() && "flex-col",
        ]}
      >
        {showcaseDef.renderNode(
          showcaseDef.createErrorBoundary(
            () => {
              caseFailed(false);
              return renderer();
            },
            errors => {
              caseFailed(true);
              return showcaseDef.attach(renderErrors(errors));
            }
          )
        )}
      </div>
    </details>
  );
}

function ShowCaseDef<TComponent, TNode>(
  def: CaseDef<TComponent>,
  showcaseDef: ShowcaseDef<TComponent, TNode>
): TNode {
  const {
    defs: propDefs,
    componentProps,
    componentSlots,
    componentEvents,
  } = prepareProps(def.props, def.slots, showcaseDef);

  const inputs = (
    <div
      class={[
        "w-full pb-2 pr-2",
        "case:!grid-cols-2 grid grid-cols-1 gap-2",
        "border-b-1 border-b-gray-300",
      ]}
    >
      {...propDefs.map(propDef => <ShowcaseField {...propDef} />)}
    </div>
  );

  return showcaseDef.instiate(showcaseDef.renderCaseSplitted as TComponent, {
    inputs: showcaseDef.attach(inputs),
    component: def.component,
    props: componentProps,
    slots: componentSlots,
    events: componentEvents,
  });
}